diff --git a/.github/workflows/check-for-updates.yml b/.github/workflows/check-for-updates.yml new file mode 100644 index 0000000..2b0cfb5 --- /dev/null +++ b/.github/workflows/check-for-updates.yml @@ -0,0 +1,111 @@ +name: Check for tzdata updates + +on: + schedule: + - cron: '0 9 * * *' # Runs daily at 9AM UTC + workflow_dispatch: + +jobs: + check-pr-exists: + runs-on: ubuntu-latest + outputs: + pr_exists: ${{ steps.check_pr_exists.outputs.pr_exists }} + steps: + - name: Check if PR already exists + id: check_pr_exists + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + PR_EXISTS=$(gh pr --repo $GITHUB_REPOSITORY \ + list --search "Update tzdata to version" \ + --json number --jq '.[] | .number') + if [ -n "$PR_EXISTS" ]; then + echo "A PR updating the tzdata version already exists: https://github.com/python/tzdata/pulls/${PR_EXISTS}" + echo "pr_exists=true" >> $GITHUB_OUTPUT + exit 0 + else + echo "pr_exists=false" >> $GITHUB_OUTPUT + fi + + check-for-updates: + runs-on: ubuntu-latest + needs: check-pr-exists + permissions: + pull-requests: write + contents: write + if: needs.check-pr-exists.outputs.pr_exists == 'false' # Run only if no PR exists + steps: + - name: Check out repository (shallow) + uses: actions/checkout@v3 + with: + fetch-depth: 1 # Shallow clone to save time + + - name: Set up Python 3.12 + uses: actions/setup-python@v4 + with: + python-version: '3.12' + + - name: Install dependencies + run: | + python -m pip install tox + sudo apt-get install gh + + - name: Run tox update + run: tox -e update + + - name: Check for repository changes and commit + id: check_changes + run: | + git config --global user.email "action@github.com" + git config --global user.name "GitHub Action" + + # Check for changes + if git diff --quiet; then + echo "No changes detected." + echo "CHANGES_DETECTED=false" >> $GITHUB_ENV + exit 0 + fi + + # Check for changes in the news.d directory + git add -A + news_files=$(git diff --cached --name-only --diff-filter=A | grep '^news.d/.*\.md' || true) + + if [ -z "$news_files" ]; then + echo "No new file in news.d, failing the job." + exit 1 + fi + + if [ $(echo "$news_files" | wc -l) -ne 1 ]; then + echo "More than one new file added in news.d, failing the job." + exit 1 + fi + echo "CHANGES_DETECTED=true" >> $GITHUB_ENV + + # Extract TZDATA_VERSION from filename + TZDATA_VERSION=$(basename "$news_files" .md) + + # Extract TZDATA_NEWS from file content + TZDATA_NEWS=$(cat "$news_files") + + echo "TZDATA_VERSION=$TZDATA_VERSION" >> $GITHUB_ENV + echo "TZDATA_NEWS=$TZDATA_NEWS" >> $GITHUB_ENV + + - name: Commit changes + id: commit_changes + if: env.CHANGES_DETECTED == 'true' + run: | + git checkout -b "updates/update_${TZDATA_VERSION}" + git commit -m "Update tzdata to version $TZDATA_VERSION" \ + -m "$TZDATA_NEWS" + git push --force origin "updates/update_${TZDATA_VERSION}" + + - name: Create pull request + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + if: env.CHANGES_DETECTED == 'true' + run: | + gh pr create --title "Update tzdata to version $TZDATA_VERSION" \ + --body "$TZDATA_NEWS" \ + --base master \ + --head $(git rev-parse --abbrev-ref HEAD) \ + --label "automatic-updates" diff --git a/update.py b/update.py index 2558607..aa760f3 100644 --- a/update.py +++ b/update.py @@ -186,6 +186,15 @@ def create_package(version: str, zonenames: Sequence[str], zoneinfo_dir: pathlib init_file.touch() +def get_current_package_version() -> str: + with open(PKG_BASE / "tzdata/__init__.py", "rt") as f: + for line in f: + if line.startswith("IANA_VERSION"): + return line.split("=", 1)[1].strip(' "\n') + + raise ValueError("IANA version not found!") + + def find_latest_version() -> str: r = requests.get(IANA_LATEST_LOCATION) fobj = io.BytesIO(r.content) @@ -426,27 +435,54 @@ def update_news(news_entry: NewsEntry): "--news-only/--no-news-only", help="Flag to disable data updates and only update the news entry", ) +@click.option( + "--skip-existing/--no-skip-existing", + default=True, + help="Whether to skip the update if we're already at the current value.", +) def main( version: str | None, news_only: bool, + skip_existing: bool, source_dir: pathlib.Path | None, ): logging.basicConfig(level=logging.INFO) - if source_dir is not None: - if version is None: - logging.error( - "--source-dir specified without --version: " - "If using --source-dir, --version must also be used." + if skip_existing: + existing_version: str | None = get_current_package_version() + else: + existing_version = None + + if version is None or version != existing_version: + if source_dir is not None: + if version is None: + logging.error( + "--source-dir specified without --version: " + "If using --source-dir, --version must also be used." + ) + sys.exit(-1) + download_locations: Sequence[pathlib.Path] | None = retrieve_local_tarballs( + version, source_dir ) - sys.exit(-1) - download_locations = retrieve_local_tarballs(version, source_dir) + else: + if version is None: + version = find_latest_version() + + if version != existing_version or not skip_existing: + download_locations = download_tzdb_tarballs(version) + else: + download_locations = None else: - if version is None: - version = find_latest_version() + download_locations = None - download_locations = download_tzdb_tarballs(version) + if skip_existing and version == existing_version: + logging.info( + f"Selected version {version} is identical " + f"to existing version {existing_version}; nothing to do!" + ) + sys.exit(0) + assert download_locations is not None tzdb_location = unpack_tzdb_tarballs(download_locations) # Update the news entry