[Author Prev][Author Next][Thread Prev][Thread Next][Author Index][Thread Index]

[tor-commits] [Git][tpo/applications/tor-browser-build][main] 8 commits: Bug 41001: Change some config files to make automation easier.



Title: GitLab

richard pushed to branch main at The Tor Project / Applications / tor-browser-build

Commits:

  • 7789b280
    by Pier Angelo Vendrame at 2024-05-07T10:41:54+00:00
    Bug 41001: Change some config files to make automation easier.
    
  • cbbcdf08
    by Pier Angelo Vendrame at 2024-05-07T10:41:54+00:00
    Bug 41001: Refactor fetch_allowed_addons.py.
    
    In this commit we change this file to be able to use it as a Python
    module from other scripts.
    
    Also, we lint it with black.
    
  • 51c8ccd3
    by Pier Angelo Vendrame at 2024-05-07T10:41:54+00:00
    Bug 41001: Refactored fetch-changelog.py.
    
    Also, added Zstandard to the possible updates.
    
  • 70539485
    by Pier Angelo Vendrame at 2024-05-07T10:41:54+00:00
    Bug 41001: Renamed fetch-changelog.py.
    
    After the refactor, fetch-changelog.py can be used as a Python module,
    but to do so we need to replace the dash in its name with something
    else (e.g., an underscore).
    
  • 48d9469a
    by Pier Angelo Vendrame at 2024-05-07T10:41:54+00:00
    Bug 41001: Refactored fetch-manual.py.
    
    Allow the script to possibly run as a Python module.
    
  • 8e155939
    by Pier Angelo Vendrame at 2024-05-07T10:41:54+00:00
    Bug 41001: Renamed fetch-manual.py to update_manual.py.
    
    This allows us to import it in other Python scripts.
    
  • 9f583c64
    by Pier Angelo Vendrame at 2024-05-07T10:41:54+00:00
    Bug 41001: Add a release preparation script.
    
  • a34dcb00
    by Pier Angelo Vendrame at 2024-05-07T10:41:54+00:00
    Bug 41001: Added a checklist to the relprep MR template.
    
    Since the release preparation is becoming an almost fully automated
    procedure, it is necessary to be more careful during the review.
    This new checklist in the release preparation MR should help to spot
    any errors.
    

16 changed files:

Changes:

  • .gitattributes
    1
    +projects/browser/allowed_addons.json -diff

  • .gitlab/issue_templates/Release Prep - Mullvad Browser Alpha.md
    ... ... @@ -67,14 +67,14 @@ Mullvad Browser Alpha (and Nightly) are on the `main` branch
    67 67
     - [ ] Update `ChangeLog-MB.txt`
    
    68 68
       - [ ] Ensure `ChangeLog-MB.txt` is sync'd between alpha and stable branches
    
    69 69
       - [ ] Check the linked issues: ask people to check if any are missing, remove the not fixed ones
    
    70
    -  - [ ] Run `./tools/fetch-changelogs.py $(ISSUE_NUMBER) --date $date $updateArgs`
    
    70
    +  - [ ] Run `./tools/fetch_changelogs.py $(ISSUE_NUMBER) --date $date $updateArgs`
    
    71 71
         - Make sure you have `requests` installed (e.g., `apt install python3-requests`)
    
    72 72
         - The first time you run this script you will need to generate an access token; the script will guide you
    
    73 73
         - `$updateArgs` should be these arguments, depending on what you actually updated:
    
    74 74
           - [ ] `--firefox` (be sure to include esr at the end if needed, which is usually the case)
    
    75 75
           - [ ] `--no-script`
    
    76 76
           - [ ] `--ublock`
    
    77
    -      - E.g., `./tools/fetch-changelogs.py 41029 --date 'December 19 2023' --firefox 115.6.0esr --no-script 11.4.29 --ublock 1.54.0`
    
    77
    +      - E.g., `./tools/fetch_changelogs.py 41029 --date 'December 19 2023' --firefox 115.6.0esr --no-script 11.4.29 --ublock 1.54.0`
    
    78 78
         - `--date $date` is optional, if omitted it will be the date on which you run the command
    
    79 79
       - [ ] Copy the output of the script to the beginning of `ChangeLog-MB.txt` and adjust its output
    
    80 80
     - [ ] Open MR with above changes, using the template for release preparations
    

  • .gitlab/issue_templates/Release Prep - Tor Browser Alpha.md
    ... ... @@ -78,6 +78,10 @@ Tor Browser Alpha (and Nightly) are on the `main` branch
    78 78
       - [ ] Check for zlib updates here: https://github.com/madler/zlib/releases
    
    79 79
         - [ ] **(Optional)** If new tag available, update `projects/zlib/config`
    
    80 80
           - [ ] `version` : update to next release tag
    
    81
    +  - [ ] Check for Zstandard updates here: https://github.com/facebook/zstd/releases
    
    82
    +    - [ ] **(Optional)** If new tag available, update `projects/zstd/config`
    
    83
    +      - [ ] `version` : update to next release tag
    
    84
    +      - [ ] `git_hash`: update to the commit corresponding to the tag (we don't check signatures for Zstandard)
    
    81 85
       - [ ] Check for tor updates here : https://gitlab.torproject.org/tpo/core/tor/-/tags
    
    82 86
         - [ ] ***(Optional)*** Update `projects/tor/config`
    
    83 87
           - [ ] `version` : update to latest `-alpha` tag or release tag if newer (ping dgoulet or ahf if unsure)
    
    ... ... @@ -86,18 +90,17 @@ Tor Browser Alpha (and Nightly) are on the `main` branch
    86 90
         - [ ] ***(Optional)*** Update `projects/go/config`
    
    87 91
           - [ ] `version` : update go version
    
    88 92
           - [ ] `input_files/sha256sum` for `go` : update sha256sum of archive (sha256 sums are displayed on the go download page)
    
    89
    -  - [ ] Check for manual updates by running (from `tor-browser-build` root): `./tools/fetch-manual.py`
    
    93
    +  - [ ] Check for manual updates by running (from `tor-browser-build` root): `./tools/update_manual.py`
    
    90 94
         - [ ] ***(Optional)*** If new version is available:
    
    91 95
           - [ ] Upload the downloaded `manual_$PIPELINEID.zip` file to `tb-build-02.torproject.org`
    
    96
    +        - The script will tell if it's necessary to
    
    92 97
           - [ ] Deploy to `tb-builder`'s `public_html` directory:
    
    93 98
             - `sudo -u tb-builder cp manual_$PIPELINEID.zip ~tb-builder/public_html/.`
    
    94
    -      - [ ] Update `projects/manual/config`:
    
    95
    -        - [ ] Change the `version` to `$PIPELINEID`
    
    96
    -        - [ ] Update `sha256sum` in the `input_files` section
    
    99
    +      - [ ] Add `projects/manual/config` to the stage area if the script updated it.
    
    97 100
     - [ ] Update `ChangeLog-TBB.txt`
    
    98 101
       - [ ] Ensure `ChangeLog-TBB.txt` is sync'd between alpha and stable branches
    
    99 102
       - [ ] Check the linked issues: ask people to check if any are missing, remove the not fixed ones
    
    100
    -  - [ ] Run `./tools/fetch-changelogs.py $(ISSUE_NUMBER) --date $date $updateArgs`
    
    103
    +  - [ ] Run `./tools/fetch_changelogs.py $(ISSUE_NUMBER) --date $date $updateArgs`
    
    101 104
         - Make sure you have `requests` installed (e.g., `apt install python3-requests`)
    
    102 105
         - The first time you run this script you will need to generate an access token; the script will guide you
    
    103 106
         - `$updateArgs` should be these arguments, depending on what you actually updated:
    
    ... ... @@ -106,8 +109,9 @@ Tor Browser Alpha (and Nightly) are on the `main` branch
    106 109
           - [ ] `--no-script`
    
    107 110
           - [ ] `--openssl`
    
    108 111
           - [ ] `--zlib`
    
    112
    +      - [ ] `--zstd`
    
    109 113
           - [ ] `--go`
    
    110
    -      - E.g., `./tools/fetch-changelogs.py 41028 --date 'December 19 2023' --firefox 115.6.0esr --tor 0.4.8.10 --no-script 11.4.29 --zlib 1.3 --go 1.21.5 --openssl 3.0.12`
    
    114
    +      - E.g., `./tools/fetch_changelogs.py 41028 --date 'December 19 2023' --firefox 115.6.0esr --tor 0.4.8.10 --no-script 11.4.29 --zlib 1.3 --go 1.21.5 --openssl 3.0.12`
    
    111 115
         - `--date $date` is optional, if omitted it will be the date on which you run the command
    
    112 116
       - [ ] Copy the output of the script to the beginning of `ChangeLog-TBB.txt` and adjust its output
    
    113 117
     - [ ] Open MR with above changes, using the template for release preparations
    

  • .gitlab/merge_request_templates/relprep.md
    1
    -## Merge Info
    
    2
    -
    
    3
    -### Related Issues
    
    1
    +## Related Issues
    
    4 2
     
    
    5 3
     - tor-browser-build#xxxxx
    
    6 4
     - tor-browser-build#xxxxx
    
    7 5
     
    
    6
    +## Self-review + reviewer's template
    
    7
    +
    
    8
    +- [ ] `rbm.conf` updates:
    
    9
    +  - [ ] `var/torbrowser_version`
    
    10
    +  - [ ] `var/torbrowser_build`: should be `build1`, unless bumping a previous release preparation
    
    11
    +  - [ ] `var/browser_release_date`: must not be in the future when we start building
    
    12
    +  - [ ] `var/torbrowser_incremental_from` (not needed for Android-only releases)
    
    13
    +- [ ] Tag updates:
    
    14
    +  - [ ] [Firefox](https://gitlab.torproject.org/tpo/applications/tor-browser/-/tags)
    
    15
    +  - [ ] Geckoview - should match Firefox
    
    16
    +  - [ ] [Firefox Android](https://gitlab.torproject.org/tpo/applications/firefox-android/-/tags)
    
    17
    +  - Tags might be speculative in the release preparation: i.e., they might not exist yet.
    
    18
    +- [ ] Addon updates:
    
    19
    +  - [ ] [NoScript](https://addons.mozilla.org/en-US/firefox/addon/noscript/)
    
    20
    +  - [ ] [uBlock Origin](https://addons.mozilla.org/en-US/firefox/addon/ublock-origin/) (Mullvad Browser only)
    
    21
    +  - [ ] [Mullvad Browser Extension](https://github.com/mullvad/browser-extension/releases) (Mullvad Browser only)
    
    22
    +  - For AMO extension (NoScript and uBlock), updating the version in the URL is not enough, check that also a numeric ID from the URL has changed
    
    23
    +- [ ] Tor and dependencies updates (Tor Browser only)
    
    24
    +  - [ ] [Tor](https://gitlab.torproject.org/tpo/core/tor/-/tags)
    
    25
    +  - [ ] [OpenSSL](https://www.openssl.org/source/): we stay on the latest LTS channel (currently 3.0.x)
    
    26
    +  - [ ] [zlib](https://github.com/madler/zlib/releases)
    
    27
    +  - [ ] [Zstandard](https://github.com/facebook/zstd/releases) (Android only, at least for now)
    
    28
    +  - [ ] [Go](https://go.dev/dl): avoid major updates, unless planned
    
    29
    +- [ ] Manual version update (Tor Browser only, optional)
    
    30
    +- [ ] Changelogs
    
    31
    +  - [ ] Changelogs must be in sync between stable and alpha
    
    32
    +  - [ ] Check the browser name
    
    33
    +  - [ ] Check the version
    
    34
    +  - [ ] Check the release date
    
    35
    +  - [ ] Check we include only the platform we're releasing for (e.g., no Android in desktop-only releases)
    
    36
    +  - [ ] Check all the updates from above are reported in the changelogs
    
    37
    +  - [ ] Check for major errors
    
    38
    +    - If you find errors such as platform or category (build system) please adjust the issue label accordingly
    
    39
    +    - You can run `tools/relprep.py --only-changelogs --date $date $version` to update only the changelogs
    
    40
    +
    
    8 41
     ## Review
    
    9 42
     
    
    10 43
     ### Request Reviewer
    

  • projects/browser/config
    ... ... @@ -111,7 +111,7 @@ input_files:
    111 111
         name: ublock-origin
    
    112 112
         sha256sum: 9928e79a52cecf7cfa231fdb0699c7d7a427660d94eb10d711ed5a2f10d2eb89
    
    113 113
         enable: '[% c("var/mullvad-browser") %]'
    
    114
    -  - URL: https://github.com/mullvad/browser-extension/releases/download/v0.9.0-firefox-beta/mullvad-browser-extension-0.9.0.xpi
    
    114
    +  - URL: https://cdn.mullvad.net/browser-extension/0.9.0/mullvad-browser-extension-0.9.0.xpi
    
    115 115
         name: mullvad-extension
    
    116 116
         sha256sum: 65bf235aa1015054ae0a54a40c5a663e67fe1d0f0799e7b4726f98cccc7f3eab
    
    117 117
         enable: '[% c("var/mullvad-browser") %]'
    

  • projects/firefox/config
    ... ... @@ -17,7 +17,8 @@ var:
    17 17
       firefox_platform_version: 115.10.0
    
    18 18
       firefox_version: '[% c("var/firefox_platform_version") %]esr'
    
    19 19
       browser_series: '13.5'
    
    20
    -  browser_branch: '[% c("var/browser_series") %]-1'
    
    20
    +  browser_rebase: 1
    
    21
    +  browser_branch: '[% c("var/browser_series") %]-[% c("var/browser_rebase") %]'
    
    21 22
       browser_build: 2
    
    22 23
       branding_directory_prefix: 'tb'
    
    23 24
       copyright_year: '[% exec("git show -s --format=%ci").remove("-.*") %]'
    

  • projects/go/config
    1 1
     # vim: filetype=yaml sw=2
    
    2
    -version: '[% IF c("var/use_go_1_20") %]1.20.14[% ELSE %]1.21.9[% END %]'
    
    2
    +# When Windows 7 goes EOL, just update this field
    
    3
    +version: '[% IF c("var/use_go_1_20") %][% c("var/go_1_20") %][% ELSE %][% c("var/go_1_21") %][% END %]'
    
    3 4
     filename: '[% project %]-[% c("version") %]-[% c("var/osname") %]-[% c("var/build_id") %].tar.[% c("compress_tar") %]'
    
    4 5
     container:
    
    5 6
       use_container: 1
    
    6 7
     
    
    7 8
     var:
    
    8 9
       use_go_1_20: 0
    
    10
    +  go_1_21: 1.21.9
    
    11
    +  go_1_20: 1.20.14
    
    9 12
       setup: |
    
    10 13
         mkdir -p /var/tmp/dist
    
    11 14
         tar -C /var/tmp/dist -xf $rootdir/[% c("go_tarfile") %]
    
    ... ... @@ -119,13 +122,11 @@ input_files:
    119 122
       - name: '[% c("var/compiler") %]'
    
    120 123
         project: '[% c("var/compiler") %]'
    
    121 124
         enable: '[% ! c("var/linux") %]'
    
    122
    -  - URL: 'https://go.dev/dl/go[% c("version") %].src.tar.gz'
    
    123
    -    # 1.21 series
    
    125
    +  - URL: 'https://go.dev/dl/go[% c("var/go_1_21") %].src.tar.gz'
    
    124 126
         name: go
    
    125 127
         sha256sum: 58f0c5ced45a0012bce2ff7a9df03e128abcc8818ebabe5027bb92bafe20e421
    
    126 128
         enable: '[% !c("var/use_go_1_20") %]'
    
    127
    -  - URL: 'https://go.dev/dl/go[% c("version") %].src.tar.gz'
    
    128
    -    # 1.20 series
    
    129
    +  - URL: 'https://go.dev/dl/go[% c("var/go_1_20") %].src.tar.gz'
    
    129 130
         name: go
    
    130 131
         sha256sum: 1aef321a0e3e38b7e91d2d7eb64040666cabdcc77d383de3c9522d0d69b67f4e
    
    131 132
         enable: '[% c("var/use_go_1_20") %]'
    

  • projects/openssl/config
    ... ... @@ -34,3 +34,4 @@ input_files:
    34 34
         project: '[% c("var/compiler") %]'
    
    35 35
       - URL: 'https://www.openssl.org/source/openssl-[% c("version") %].tar.gz'
    
    36 36
         sha256sum: 88525753f79d3bec27d2fa7c66aa0b92b3aa9498dafd93d7cfa4b3780cdae313
    
    37
    +    name: openssl

  • rbm.conf
    ... ... @@ -75,16 +75,16 @@ buildconf:
    75 75
     var:
    
    76 76
       torbrowser_version: '13.5a7'
    
    77 77
       torbrowser_build: 'build2'
    
    78
    -  torbrowser_incremental_from:
    
    79
    -    - '13.5a6'
    
    80
    -    - '13.5a5'
    
    81
    -    - '13.5a4'
    
    82 78
       # This should be the date of when the build is started. For the build
    
    83 79
       # to be reproducible, browser_release_date should always be in the past.
    
    84 80
       browser_release_date: '2024/04/25 12:00:00'
    
    85 81
       browser_release_date_timestamp: '[% USE date; date.format(c("var/browser_release_date"), "%s") %]'
    
    86 82
       updater_enabled: 1
    
    87 83
       build_mar: 1
    
    84
    +  torbrowser_incremental_from:
    
    85
    +    - '13.5a6'
    
    86
    +    - '13.5a5'
    
    87
    +    - '13.5a4'
    
    88 88
       mar_channel_id: '[% c("var/projectname") %]-torproject-[% c("var/channel") %]'
    
    89 89
     
    
    90 90
       # By default, we sort the list of installed packages. This allows sharing
    

  • tools/.gitignore
    1 1
     _repackaged
    
    2
    +__pycache__
    
    2 3
     .changelogs_token
    
    3 4
     local

  • tools/fetch-changelogs.py deleted
    1
    -#!/usr/bin/env python3
    
    2
    -import argparse
    
    3
    -from datetime import datetime
    
    4
    -import enum
    
    5
    -from pathlib import Path
    
    6
    -import re
    
    7
    -import sys
    
    8
    -
    
    9
    -import requests
    
    10
    -
    
    11
    -
    
    12
    -GITLAB = "https://gitlab.torproject.org"
    
    13
    -API_URL = f"{GITLAB}/api/v4"
    
    14
    -PROJECT_ID = 473
    
    15
    -
    
    16
    -is_mb = False
    
    17
    -project_order = {
    
    18
    -    "tor-browser-spec": 0,
    
    19
    -    # Leave 1 free, so we can redefine mullvad-browser when needed.
    
    20
    -    "tor-browser": 2,
    
    21
    -    "tor-browser-build": 3,
    
    22
    -    "mullvad-browser": 4,
    
    23
    -    "rbm": 5,
    
    24
    -}
    
    25
    -
    
    26
    -
    
    27
    -class EntryType(enum.IntFlag):
    
    28
    -    UPDATE = 0
    
    29
    -    ISSUE = 1
    
    30
    -
    
    31
    -
    
    32
    -class Platform(enum.IntFlag):
    
    33
    -    WINDOWS = 8
    
    34
    -    MACOS = 4
    
    35
    -    LINUX = 2
    
    36
    -    ANDROID = 1
    
    37
    -    DESKTOP = 8 | 4 | 2
    
    38
    -    ALL_PLATFORMS = 8 | 4 | 2 | 1
    
    39
    -
    
    40
    -
    
    41
    -class ChangelogEntry:
    
    42
    -    def __init__(self, type_, platform, num_platforms, is_build):
    
    43
    -        self.type = type_
    
    44
    -        self.platform = platform
    
    45
    -        self.num_platforms = num_platforms
    
    46
    -        self.is_build = is_build
    
    47
    -
    
    48
    -    def get_platforms(self):
    
    49
    -        if self.platform == Platform.ALL_PLATFORMS:
    
    50
    -            return "All Platforms"
    
    51
    -        platforms = []
    
    52
    -        if self.platform & Platform.WINDOWS:
    
    53
    -            platforms.append("Windows")
    
    54
    -        if self.platform & Platform.MACOS:
    
    55
    -            platforms.append("macOS")
    
    56
    -        if self.platform & Platform.LINUX:
    
    57
    -            platforms.append("Linux")
    
    58
    -        if self.platform & Platform.ANDROID:
    
    59
    -            platforms.append("Android")
    
    60
    -        return " + ".join(platforms)
    
    61
    -
    
    62
    -    def __lt__(self, other):
    
    63
    -        if self.type != other.type:
    
    64
    -            return self.type < other.type
    
    65
    -        if self.type == EntryType.UPDATE:
    
    66
    -            # Rely on sorting being stable on Python
    
    67
    -            return False
    
    68
    -        if self.project == other.project:
    
    69
    -            return self.number < other.number
    
    70
    -        return project_order[self.project] < project_order[other.project]
    
    71
    -
    
    72
    -
    
    73
    -class UpdateEntry(ChangelogEntry):
    
    74
    -    def __init__(self, name, version):
    
    75
    -        if name == "Firefox" and not is_mb:
    
    76
    -            platform = Platform.DESKTOP
    
    77
    -            num_platforms = 3
    
    78
    -        elif name == "GeckoView":
    
    79
    -            platform = Platform.ANDROID
    
    80
    -            num_platforms = 3
    
    81
    -        else:
    
    82
    -            platform = Platform.ALL_PLATFORMS
    
    83
    -            num_platforms = 4
    
    84
    -        super().__init__(
    
    85
    -            EntryType.UPDATE, platform, num_platforms, name == "Go"
    
    86
    -        )
    
    87
    -        self.name = name
    
    88
    -        self.version = version
    
    89
    -
    
    90
    -    def __str__(self):
    
    91
    -        return f"Updated {self.name} to {self.version}"
    
    92
    -
    
    93
    -
    
    94
    -class Issue(ChangelogEntry):
    
    95
    -    def __init__(self, j):
    
    96
    -        self.title = j["title"]
    
    97
    -        self.project, self.number = (
    
    98
    -            j["references"]["full"].rsplit("/", 2)[-1].split("#")
    
    99
    -        )
    
    100
    -        self.number = int(self.number)
    
    101
    -        platform = 0
    
    102
    -        num_platforms = 0
    
    103
    -        if "Desktop" in j["labels"]:
    
    104
    -            platform = Platform.DESKTOP
    
    105
    -            num_platforms += 3
    
    106
    -        else:
    
    107
    -            if "Windows" in j["labels"]:
    
    108
    -                platform |= Platform.WINDOWS
    
    109
    -                num_platforms += 1
    
    110
    -            if "MacOS" in j["labels"]:
    
    111
    -                platform |= Platform.MACOS
    
    112
    -                num_platforms += 1
    
    113
    -            if "Linux" in j["labels"]:
    
    114
    -                platform |= Platform.LINUX
    
    115
    -                num_platforms += 1
    
    116
    -        if "Android" in j["labels"]:
    
    117
    -            if is_mb and num_platforms == 0:
    
    118
    -                raise Exception(
    
    119
    -                    f"Android-only issue on Mullvad Browser: {j['references']['full']}!"
    
    120
    -                )
    
    121
    -            elif not is_mb:
    
    122
    -                platform |= Platform.ANDROID
    
    123
    -                num_platforms += 1
    
    124
    -        if not platform or (is_mb and platform == Platform.DESKTOP):
    
    125
    -            platform = Platform.ALL_PLATFORMS
    
    126
    -            num_platforms = 4
    
    127
    -        is_build = "Build System" in j["labels"]
    
    128
    -        super().__init__(EntryType.ISSUE, platform, num_platforms, is_build)
    
    129
    -
    
    130
    -    def __str__(self):
    
    131
    -        return f"Bug {self.number}: {self.title} [{self.project}]"
    
    132
    -
    
    133
    -
    
    134
    -def sorted_issues(issues):
    
    135
    -    issues = [sorted(v) for v in issues.values()]
    
    136
    -    return sorted(
    
    137
    -        issues,
    
    138
    -        key=lambda group: (group[0].num_platforms << 8) | group[0].platform,
    
    139
    -        reverse=True,
    
    140
    -    )
    
    141
    -
    
    142
    -
    
    143
    -parser = argparse.ArgumentParser()
    
    144
    -parser.add_argument("issue_version")
    
    145
    -parser.add_argument("--date", help="The date of the release")
    
    146
    -parser.add_argument("--firefox", help="New Firefox version (if we rebased)")
    
    147
    -parser.add_argument("--tor", help="New Tor version (if updated)")
    
    148
    -parser.add_argument("--no-script", help="New NoScript version (if updated)")
    
    149
    -parser.add_argument("--openssl", help="New OpenSSL version (if updated)")
    
    150
    -parser.add_argument("--ublock", help="New uBlock version (if updated)")
    
    151
    -parser.add_argument("--zlib", help="New zlib version (if updated)")
    
    152
    -parser.add_argument("--go", help="New Go version (if updated)")
    
    153
    -args = parser.parse_args()
    
    154
    -
    
    155
    -if not args.issue_version:
    
    156
    -    parser.print_help()
    
    157
    -    sys.exit(1)
    
    158
    -
    
    159
    -token_file = Path(__file__).parent / ".changelogs_token"
    
    160
    -if not token_file.exists():
    
    161
    -    print(
    
    162
    -        f"Please add your personal GitLab token (with 'read_api' scope) to {token_file}"
    
    163
    -    )
    
    164
    -    print(
    
    165
    -        f"Please go to {GITLAB}/-/profile/personal_access_tokens and generate it."
    
    166
    -    )
    
    167
    -    token = input("Please enter the new token: ").strip()
    
    168
    -    if not token:
    
    169
    -        print("Invalid token!")
    
    170
    -        sys.exit(2)
    
    171
    -    with token_file.open("w") as f:
    
    172
    -        f.write(token)
    
    173
    -with token_file.open() as f:
    
    174
    -    token = f.read().strip()
    
    175
    -headers = {"PRIVATE-TOKEN": token}
    
    176
    -
    
    177
    -version = args.issue_version
    
    178
    -r = requests.get(
    
    179
    -    f"{API_URL}/projects/{PROJECT_ID}/issues?labels=Release Prep",
    
    180
    -    headers=headers,
    
    181
    -)
    
    182
    -if r.status_code == 401:
    
    183
    -    print("Unauthorized! Has your token expired?")
    
    184
    -    sys.exit(3)
    
    185
    -issue = None
    
    186
    -issues = []
    
    187
    -for i in r.json():
    
    188
    -    if i["title"].find(version) != -1:
    
    189
    -        issues.append(i)
    
    190
    -if len(issues) == 1:
    
    191
    -    issue = issues[0]
    
    192
    -elif len(issues) > 1:
    
    193
    -    print("More than one matching issue found:")
    
    194
    -    for idx, i in enumerate(issues):
    
    195
    -        print(f"  {idx + 1}) #{i['iid']} - {i['title']}")
    
    196
    -    print("Please use the issue id.")
    
    197
    -    sys.exit(4)
    
    198
    -else:
    
    199
    -    iid = version
    
    200
    -    version = "CHANGEME!"
    
    201
    -    if iid[0] == "#":
    
    202
    -        iid = iid[1:]
    
    203
    -    try:
    
    204
    -        int(iid)
    
    205
    -        r = requests.get(
    
    206
    -            f"{API_URL}/projects/{PROJECT_ID}/issues?iids={iid}",
    
    207
    -            headers=headers,
    
    208
    -        )
    
    209
    -        if r.ok and r.json():
    
    210
    -            issue = r.json()[0]
    
    211
    -            version_match = re.search(r"\b[0-9]+\.[.0-9a]+\b", issue["title"])
    
    212
    -            if version_match:
    
    213
    -                version = version_match.group()
    
    214
    -    except ValueError:
    
    215
    -        pass
    
    216
    -if not issue:
    
    217
    -    print(
    
    218
    -        "Release preparation issue not found. Please make sure it has ~Release Prep."
    
    219
    -    )
    
    220
    -    sys.exit(5)
    
    221
    -if "Sponsor 131" in issue["labels"]:
    
    222
    -    is_mb = True
    
    223
    -    project_order["mullvad-browser"] = 1
    
    224
    -iid = issue["iid"]
    
    225
    -
    
    226
    -linked = {}
    
    227
    -linked_build = {}
    
    228
    -
    
    229
    -
    
    230
    -def add_entry(entry):
    
    231
    -    target = linked_build if entry.is_build else linked
    
    232
    -    if entry.platform not in target:
    
    233
    -        target[entry.platform] = []
    
    234
    -    target[entry.platform].append(entry)
    
    235
    -
    
    236
    -
    
    237
    -if args.firefox:
    
    238
    -    add_entry(UpdateEntry("Firefox", args.firefox))
    
    239
    -    if not is_mb:
    
    240
    -        add_entry(UpdateEntry("GeckoView", args.firefox))
    
    241
    -if args.tor and not is_mb:
    
    242
    -    add_entry(UpdateEntry("Tor", args.tor))
    
    243
    -if args.no_script:
    
    244
    -    add_entry(UpdateEntry("NoScript", args.no_script))
    
    245
    -if not is_mb:
    
    246
    -    if args.openssl:
    
    247
    -        add_entry(UpdateEntry("OpenSSL", args.openssl))
    
    248
    -    if args.zlib:
    
    249
    -        add_entry(UpdateEntry("zlib", args.zlib))
    
    250
    -    if args.go:
    
    251
    -        add_entry(UpdateEntry("Go", args.go))
    
    252
    -elif args.ublock:
    
    253
    -    add_entry(UpdateEntry("uBlock Origin", args.ublock))
    
    254
    -
    
    255
    -r = requests.get(
    
    256
    -    f"{API_URL}/projects/{PROJECT_ID}/issues/{iid}/links", headers=headers
    
    257
    -)
    
    258
    -for i in r.json():
    
    259
    -    add_entry(Issue(i))
    
    260
    -
    
    261
    -linked = sorted_issues(linked)
    
    262
    -linked_build = sorted_issues(linked_build)
    
    263
    -
    
    264
    -name = "Mullvad" if is_mb else "Tor"
    
    265
    -date = args.date if args.date else datetime.now().strftime("%B %d %Y")
    
    266
    -print(f"{name} Browser {version} - {date}")
    
    267
    -for issues in linked:
    
    268
    -    print(f" * {issues[0].get_platforms()}")
    
    269
    -    for i in issues:
    
    270
    -        print(f"   * {i}")
    
    271
    -if linked_build:
    
    272
    -    print(" * Build System")
    
    273
    -    for issues in linked_build:
    
    274
    -        print(f"   * {issues[0].get_platforms()}")
    
    275
    -        for i in issues:
    
    276
    -            print(f"     * {i}")

  • tools/fetch-manual.py deleted
    1
    -#!/usr/bin/env python3
    
    2
    -import hashlib
    
    3
    -from pathlib import Path
    
    4
    -import sys
    
    5
    -
    
    6
    -import requests
    
    7
    -import yaml
    
    8
    -
    
    9
    -
    
    10
    -GITLAB = "https://gitlab.torproject.org"
    
    11
    -API_URL = f"{GITLAB}/api/v4"
    
    12
    -PROJECT_ID = 23
    
    13
    -REF_NAME = "main"
    
    14
    -
    
    15
    -
    
    16
    -token_file = Path(__file__).parent / ".changelogs_token"
    
    17
    -if not token_file.exists():
    
    18
    -    print("This scripts uses the same access token as fetch-changelog.py.")
    
    19
    -    print("However, the file has not been found.")
    
    20
    -    print(
    
    21
    -        "Please run fetch-changelog.py to get the instructions on how to "
    
    22
    -        "generate it."
    
    23
    -    )
    
    24
    -    sys.exit(1)
    
    25
    -with token_file.open() as f:
    
    26
    -    headers = {"PRIVATE-TOKEN": f.read().strip()}
    
    27
    -
    
    28
    -r = requests.get(f"{API_URL}/projects/{PROJECT_ID}/jobs", headers=headers)
    
    29
    -if r.status_code == 401:
    
    30
    -    print("Unauthorized! Maybe the token has expired.")
    
    31
    -    sys.exit(2)
    
    32
    -found = False
    
    33
    -for job in r.json():
    
    34
    -    if job["ref"] != REF_NAME:
    
    35
    -        continue
    
    36
    -    for art in job["artifacts"]:
    
    37
    -        if art["filename"] == "artifacts.zip":
    
    38
    -            found = True
    
    39
    -            break
    
    40
    -    if found:
    
    41
    -        break
    
    42
    -if not found:
    
    43
    -    print("Cannot find a usable job.")
    
    44
    -    sys.exit(3)
    
    45
    -
    
    46
    -pipeline_id = job["pipeline"]["id"]
    
    47
    -conf_file = Path(__file__).parent.parent / "projects/manual/config"
    
    48
    -with conf_file.open() as f:
    
    49
    -    config = yaml.load(f, yaml.SafeLoader)
    
    50
    -if int(config["version"]) == int(pipeline_id):
    
    51
    -    print(
    
    52
    -        "projects/manual/config is already using the latest pipeline. Nothing to do."
    
    53
    -    )
    
    54
    -    sys.exit(0)
    
    55
    -
    
    56
    -manual_dir = Path(__file__).parent.parent / "out/manual"
    
    57
    -manual_dir.mkdir(0o755, parents=True, exist_ok=True)
    
    58
    -manual_file = manual_dir / f"manual_{pipeline_id}.zip"
    
    59
    -sha256 = hashlib.sha256()
    
    60
    -if manual_file.exists():
    
    61
    -    with manual_file.open("rb") as f:
    
    62
    -        while chunk := f.read(8192):
    
    63
    -            sha256.update(chunk)
    
    64
    -    print("You already have the latest manual version in your out directory.")
    
    65
    -    print("Please update projects/manual/config to:")
    
    66
    -else:
    
    67
    -    print("Downloading the new version of the manual...")
    
    68
    -    url = f"{API_URL}/projects/{PROJECT_ID}/jobs/artifacts/{REF_NAME}/download?job={job['name']}"
    
    69
    -    r = requests.get(url, headers=headers, stream=True)
    
    70
    -    # https://stackoverflow.com/a/16696317
    
    71
    -    r.raise_for_status()
    
    72
    -    with manual_file.open("wb") as f:
    
    73
    -        for chunk in r.iter_content(chunk_size=8192):
    
    74
    -            f.write(chunk)
    
    75
    -            sha256.update(chunk)
    
    76
    -    print(f"File downloaded as {manual_file}.")
    
    77
    -    print(
    
    78
    -        "Please upload it to tb-build-02.torproject.org:~tb-builder/public_html/. and then update projects/manual/config:"
    
    79
    -    )
    
    80
    -sha256 = sha256.hexdigest()
    
    81
    -
    
    82
    -print(f"\tversion: {pipeline_id}")
    
    83
    -print(f"\tSHA256: {sha256}")

  • tools/fetch_allowed_addons.py
    ... ... @@ -5,33 +5,49 @@ import json
    5 5
     import base64
    
    6 6
     import sys
    
    7 7
     
    
    8
    +NOSCRIPT = "{73a6fe31-595d-460b-a920-fcc0f8843232}"
    
    9
    +
    
    10
    +
    
    8 11
     def fetch(x):
    
    9
    -  with urllib.request.urlopen(x) as response:
    
    10
    -    return response.read()
    
    12
    +    with urllib.request.urlopen(x) as response:
    
    13
    +        return response.read()
    
    14
    +
    
    11 15
     
    
    12 16
     def find_addon(addons, addon_id):
    
    13
    -  results = addons['results']
    
    14
    -  for x in results:
    
    15
    -    addon = x['addon']
    
    16
    -    if addon['guid'] == addon_id:
    
    17
    -      return addon
    
    18
    -  sys.exit("Error: cannot find addon " + addon_id)
    
    17
    +    results = addons["results"]
    
    18
    +    for x in results:
    
    19
    +        addon = x["addon"]
    
    20
    +        if addon["guid"] == addon_id:
    
    21
    +            return addon
    
    22
    +
    
    19 23
     
    
    20 24
     def fetch_and_embed_icons(addons):
    
    21
    -  results = addons['results']
    
    22
    -  for x in results:
    
    23
    -    addon = x['addon']
    
    24
    -    icon_data = fetch(addon['icon_url'])
    
    25
    -    addon['icon_url'] = 'data:image/png;base64,' + str(base64.b64encode(icon_data), 'utf8')
    
    25
    +    results = addons["results"]
    
    26
    +    for x in results:
    
    27
    +        addon = x["addon"]
    
    28
    +        icon_data = fetch(addon["icon_url"])
    
    29
    +        addon["icon_url"] = "data:image/png;base64," + str(
    
    30
    +            base64.b64encode(icon_data), "utf8"
    
    31
    +        )
    
    32
    +
    
    33
    +
    
    34
    +def fetch_allowed_addons(amo_collection=None):
    
    35
    +    if amo_collection is None:
    
    36
    +        amo_collection = "83a9cccfe6e24a34bd7b155ff9ee32"
    
    37
    +    url = f"https://services.addons.mozilla.org/api/v4/accounts/account/mozilla/collections/{amo_collection}/addons/"
    
    38
    +    data = json.loads(fetch(url))
    
    39
    +    fetch_and_embed_icons(data)
    
    40
    +    data["results"].sort(key=lambda x: x["addon"]["guid"])
    
    41
    +    return data
    
    42
    +
    
    26 43
     
    
    27 44
     def main(argv):
    
    28
    -  amo_collection = argv[0] if argv else '83a9cccfe6e24a34bd7b155ff9ee32'
    
    29
    -  url = 'https://services.addons.mozilla.org/api/v4/accounts/account/mozilla/collections/' + amo_collection + '/addons/'
    
    30
    -  data = json.loads(fetch(url))
    
    31
    -  fetch_and_embed_icons(data)
    
    32
    -  data['results'].sort(key=lambda x: x['addon']['guid'])
    
    33
    -  find_addon(data, '{73a6fe31-595d-460b-a920-fcc0f8843232}') # Check that NoScript is present
    
    34
    -  print(json.dumps(data, indent=2, ensure_ascii=False))
    
    45
    +    data = fetch_allowed_addons(argv[0] if len(argv) > 1 else None)
    
    46
    +    # Check that NoScript is present
    
    47
    +    if find_addon(data, NOSCRIPT) is None:
    
    48
    +        sys.exit("Error: cannot find NoScript.")
    
    49
    +    print(json.dumps(data, indent=2, ensure_ascii=False))
    
    50
    +
    
    35 51
     
    
    36 52
     if __name__ == "__main__":
    
    37
    -   main(sys.argv[1:])
    53
    +    main(sys.argv[1:])

  • tools/fetch_changelogs.py
    1
    +#!/usr/bin/env python3
    
    2
    +import argparse
    
    3
    +from datetime import datetime
    
    4
    +import enum
    
    5
    +from pathlib import Path
    
    6
    +import re
    
    7
    +import sys
    
    8
    +
    
    9
    +import requests
    
    10
    +
    
    11
    +
    
    12
    +GITLAB = "https://gitlab.torproject.org"
    
    13
    +API_URL = f"{GITLAB}/api/v4"
    
    14
    +PROJECT_ID = 473
    
    15
    +AUTH_HEADER = "PRIVATE-TOKEN"
    
    16
    +
    
    17
    +
    
    18
    +class EntryType(enum.IntFlag):
    
    19
    +    UPDATE = 0
    
    20
    +    ISSUE = 1
    
    21
    +
    
    22
    +
    
    23
    +class Platform(enum.IntFlag):
    
    24
    +    WINDOWS = 8
    
    25
    +    MACOS = 4
    
    26
    +    LINUX = 2
    
    27
    +    ANDROID = 1
    
    28
    +    DESKTOP = 8 | 4 | 2
    
    29
    +    ALL_PLATFORMS = 8 | 4 | 2 | 1
    
    30
    +
    
    31
    +
    
    32
    +class ChangelogEntry:
    
    33
    +    def __init__(self, type_, platform, num_platforms, is_build, is_mb):
    
    34
    +        self.type = type_
    
    35
    +        self.platform = platform
    
    36
    +        self.num_platforms = num_platforms
    
    37
    +        self.is_build = is_build
    
    38
    +        self.project_order = {
    
    39
    +            "tor-browser-spec": 0,
    
    40
    +            # Leave 1 free, so we can redefine mullvad-browser when needed.
    
    41
    +            "tor-browser": 2,
    
    42
    +            "tor-browser-build": 3,
    
    43
    +            "mullvad-browser": 1 if is_mb else 4,
    
    44
    +            "rbm": 5,
    
    45
    +        }
    
    46
    +
    
    47
    +    def get_platforms(self):
    
    48
    +        if self.platform == Platform.ALL_PLATFORMS:
    
    49
    +            return "All Platforms"
    
    50
    +        platforms = []
    
    51
    +        if self.platform & Platform.WINDOWS:
    
    52
    +            platforms.append("Windows")
    
    53
    +        if self.platform & Platform.MACOS:
    
    54
    +            platforms.append("macOS")
    
    55
    +        if self.platform & Platform.LINUX:
    
    56
    +            platforms.append("Linux")
    
    57
    +        if self.platform & Platform.ANDROID:
    
    58
    +            platforms.append("Android")
    
    59
    +        return " + ".join(platforms)
    
    60
    +
    
    61
    +    def __lt__(self, other):
    
    62
    +        if self.num_platforms != other.num_platforms:
    
    63
    +            return self.num_platforms > other.num_platforms
    
    64
    +        if self.platform != other.platform:
    
    65
    +            return self.platform > other.platform
    
    66
    +        if self.type != other.type:
    
    67
    +            return self.type < other.type
    
    68
    +        if self.type == EntryType.UPDATE:
    
    69
    +            # Rely on sorting being stable on Python
    
    70
    +            return False
    
    71
    +        if self.project == other.project:
    
    72
    +            return self.number < other.number
    
    73
    +        return (
    
    74
    +            self.project_order[self.project]
    
    75
    +            < self.project_order[other.project]
    
    76
    +        )
    
    77
    +
    
    78
    +
    
    79
    +class UpdateEntry(ChangelogEntry):
    
    80
    +    def __init__(self, name, version, is_mb):
    
    81
    +        if name == "Firefox" and not is_mb:
    
    82
    +            platform = Platform.DESKTOP
    
    83
    +            num_platforms = 3
    
    84
    +        elif name == "GeckoView" or name == "Zstandard":
    
    85
    +            platform = Platform.ANDROID
    
    86
    +            num_platforms = 1
    
    87
    +        else:
    
    88
    +            platform = Platform.ALL_PLATFORMS
    
    89
    +            num_platforms = 4
    
    90
    +        super().__init__(
    
    91
    +            EntryType.UPDATE, platform, num_platforms, name == "Go", is_mb
    
    92
    +        )
    
    93
    +        self.name = name
    
    94
    +        self.version = version
    
    95
    +
    
    96
    +    def __str__(self):
    
    97
    +        return f"Updated {self.name} to {self.version}"
    
    98
    +
    
    99
    +
    
    100
    +class Issue(ChangelogEntry):
    
    101
    +    def __init__(self, j, is_mb):
    
    102
    +        self.title = j["title"]
    
    103
    +        self.project, self.number = (
    
    104
    +            j["references"]["full"].rsplit("/", 2)[-1].split("#")
    
    105
    +        )
    
    106
    +        self.number = int(self.number)
    
    107
    +        platform = 0
    
    108
    +        num_platforms = 0
    
    109
    +        if "Desktop" in j["labels"]:
    
    110
    +            platform = Platform.DESKTOP
    
    111
    +            num_platforms += 3
    
    112
    +        else:
    
    113
    +            if "Windows" in j["labels"]:
    
    114
    +                platform |= Platform.WINDOWS
    
    115
    +                num_platforms += 1
    
    116
    +            if "MacOS" in j["labels"]:
    
    117
    +                platform |= Platform.MACOS
    
    118
    +                num_platforms += 1
    
    119
    +            if "Linux" in j["labels"]:
    
    120
    +                platform |= Platform.LINUX
    
    121
    +                num_platforms += 1
    
    122
    +        if "Android" in j["labels"]:
    
    123
    +            if is_mb and num_platforms == 0:
    
    124
    +                raise Exception(
    
    125
    +                    f"Android-only issue on Mullvad Browser: {j['references']['full']}!"
    
    126
    +                )
    
    127
    +            elif not is_mb:
    
    128
    +                platform |= Platform.ANDROID
    
    129
    +                num_platforms += 1
    
    130
    +        if not platform or (is_mb and platform == Platform.DESKTOP):
    
    131
    +            platform = Platform.ALL_PLATFORMS
    
    132
    +            num_platforms = 4
    
    133
    +        is_build = "Build System" in j["labels"]
    
    134
    +        super().__init__(
    
    135
    +            EntryType.ISSUE, platform, num_platforms, is_build, is_mb
    
    136
    +        )
    
    137
    +
    
    138
    +    def __str__(self):
    
    139
    +        return f"Bug {self.number}: {self.title} [{self.project}]"
    
    140
    +
    
    141
    +
    
    142
    +class ChangelogBuilder:
    
    143
    +
    
    144
    +    def __init__(self, auth_token, issue_or_version, is_mullvad=None):
    
    145
    +        self.headers = {AUTH_HEADER: auth_token}
    
    146
    +        self._find_issue(issue_or_version, is_mullvad)
    
    147
    +
    
    148
    +    def _find_issue(self, issue_or_version, is_mullvad):
    
    149
    +        self.version = None
    
    150
    +        if issue_or_version[0] == "#":
    
    151
    +            self._fetch_issue(issue_or_version[1:], is_mullvad)
    
    152
    +            return
    
    153
    +        labels = "Release Prep"
    
    154
    +        if is_mullvad:
    
    155
    +            labels += ",Sponsor 131"
    
    156
    +        elif not is_mullvad and is_mullvad is not None:
    
    157
    +            labels += "&not[labels]=Sponsor 131"
    
    158
    +        r = requests.get(
    
    159
    +            f"{API_URL}/projects/{PROJECT_ID}/issues?labels={labels}&search={issue_or_version}&in=title",
    
    160
    +            headers=self.headers,
    
    161
    +        )
    
    162
    +        r.raise_for_status()
    
    163
    +        issues = r.json()
    
    164
    +        if len(issues) == 1:
    
    165
    +            self.version = issue_or_version
    
    166
    +            self._set_issue(issues[0], is_mullvad)
    
    167
    +        elif len(issues) > 1:
    
    168
    +            raise ValueError(
    
    169
    +                "Multiple issues found, try to specify the browser."
    
    170
    +            )
    
    171
    +        else:
    
    172
    +            self._fetch_issue(issue_or_version, is_mullvad)
    
    173
    +
    
    174
    +    def _fetch_issue(self, number, is_mullvad):
    
    175
    +        try:
    
    176
    +            # Validate the string to be an integer
    
    177
    +            number = int(number)
    
    178
    +        except ValueError:
    
    179
    +            # This is called either as a last chance, or because we
    
    180
    +            # were given "#", so this error should be good.
    
    181
    +            raise ValueError("Issue not found")
    
    182
    +        r = requests.get(
    
    183
    +            f"{API_URL}/projects/{PROJECT_ID}/issues?iids[]={number}",
    
    184
    +            headers=self.headers,
    
    185
    +        )
    
    186
    +        r.raise_for_status()
    
    187
    +        issues = r.json()
    
    188
    +        if len(issues) != 1:
    
    189
    +            # It should be only 0, since we used the number...
    
    190
    +            raise ValueError("Issue not found")
    
    191
    +        self._set_issue(issues[0], is_mullvad)
    
    192
    +
    
    193
    +    def _set_issue(self, issue, is_mullvad):
    
    194
    +        has_s131 = "Sponsor 131" in issue["labels"]
    
    195
    +        if is_mullvad is not None and is_mullvad != has_s131:
    
    196
    +            raise ValueError(
    
    197
    +                "Inconsistency detected: a browser was explicitly specified, but the issue does not have the correct labels."
    
    198
    +            )
    
    199
    +        self.issue_id = issue["iid"]
    
    200
    +        self.is_mullvad = has_s131
    
    201
    +
    
    202
    +        if self.version is None:
    
    203
    +            version_match = re.search(r"\b[0-9]+\.[.0-9a]+\b", issue["title"])
    
    204
    +            if version_match:
    
    205
    +                self.version = version_match.group()
    
    206
    +
    
    207
    +    def create(self, **kwargs):
    
    208
    +        self._find_linked()
    
    209
    +        self._add_updates(kwargs)
    
    210
    +        self._sort_issues()
    
    211
    +        name = "Mullvad" if self.is_mullvad else "Tor"
    
    212
    +        date = (
    
    213
    +            kwargs["date"]
    
    214
    +            if kwargs.get("date")
    
    215
    +            else datetime.now().strftime("%B %d %Y")
    
    216
    +        )
    
    217
    +        text = f"{name} Browser {self.version} - {date}\n"
    
    218
    +        prev_platform = ""
    
    219
    +        for issue in self.issues:
    
    220
    +            platform = issue.get_platforms()
    
    221
    +            if platform != prev_platform:
    
    222
    +                text += f" * {platform}\n"
    
    223
    +                prev_platform = platform
    
    224
    +            text += f"   * {issue}\n"
    
    225
    +        if self.issues_build:
    
    226
    +            text += " * Build System\n"
    
    227
    +            prev_platform = ""
    
    228
    +            for issue in self.issues_build:
    
    229
    +                platform = issue.get_platforms()
    
    230
    +                if platform != prev_platform:
    
    231
    +                    text += f"   * {platform}\n"
    
    232
    +                    prev_platform = platform
    
    233
    +                text += f"     * {issue}\n"
    
    234
    +        return text
    
    235
    +
    
    236
    +    def _find_linked(self):
    
    237
    +        self.issues = []
    
    238
    +        self.issues_build = []
    
    239
    +
    
    240
    +        r = requests.get(
    
    241
    +            f"{API_URL}/projects/{PROJECT_ID}/issues/{self.issue_id}/links",
    
    242
    +            headers=self.headers,
    
    243
    +        )
    
    244
    +        for i in r.json():
    
    245
    +            self._add_issue(i)
    
    246
    +
    
    247
    +    def _add_issue(self, gitlab_data):
    
    248
    +        self._add_entry(Issue(gitlab_data, self.is_mullvad))
    
    249
    +
    
    250
    +    def _add_entry(self, entry):
    
    251
    +        target = self.issues_build if entry.is_build else self.issues
    
    252
    +        target.append(entry)
    
    253
    +
    
    254
    +    def _add_updates(self, updates):
    
    255
    +        names = {
    
    256
    +            "Firefox": "firefox",
    
    257
    +        }
    
    258
    +        if not self.is_mullvad:
    
    259
    +            names.update(
    
    260
    +                {
    
    261
    +                    "GeckoView": "firefox",
    
    262
    +                    "Tor": "tor",
    
    263
    +                    "NoScript": "noscript",
    
    264
    +                    "OpenSSL": "openssl",
    
    265
    +                    "zlib": "zlib",
    
    266
    +                    "Zstandard": "zstd",
    
    267
    +                    "Go": "go",
    
    268
    +                }
    
    269
    +            )
    
    270
    +        else:
    
    271
    +            names.update(
    
    272
    +                {
    
    273
    +                    "Mullvad Browser Extension": "mb_extension",
    
    274
    +                    "uBlock Origin": "ublock",
    
    275
    +                }
    
    276
    +            )
    
    277
    +        for name, key in names.items():
    
    278
    +            self._maybe_add_update(name, updates, key)
    
    279
    +
    
    280
    +    def _maybe_add_update(self, name, updates, key):
    
    281
    +        if updates.get(key):
    
    282
    +            self._add_entry(UpdateEntry(name, updates[key], self.is_mullvad))
    
    283
    +
    
    284
    +    def _sort_issues(self):
    
    285
    +        self.issues.sort()
    
    286
    +        self.issues_build.sort()
    
    287
    +
    
    288
    +
    
    289
    +def load_token(test=True, interactive=True):
    
    290
    +    token_path = Path(__file__).parent / ".changelogs_token"
    
    291
    +
    
    292
    +    if token_path.exists():
    
    293
    +        with token_path.open() as f:
    
    294
    +            token = f.read().strip()
    
    295
    +    elif interactive:
    
    296
    +        print(
    
    297
    +            f"Please add your personal GitLab token (with 'read_api' scope) to {token_path}"
    
    298
    +        )
    
    299
    +        print(
    
    300
    +            f"Please go to {GITLAB}/-/profile/personal_access_tokens and generate it."
    
    301
    +        )
    
    302
    +        token = input("Please enter the new token: ").strip()
    
    303
    +        if not token:
    
    304
    +            raise ValueError("Invalid token!")
    
    305
    +        with token_path.open("w") as f:
    
    306
    +            f.write(token)
    
    307
    +    if test:
    
    308
    +        r = requests.get(f"{API_URL}/version", headers={AUTH_HEADER: token})
    
    309
    +        if r.status_code == 401:
    
    310
    +            raise ValueError("The loaded or provided token does not work.")
    
    311
    +    return token
    
    312
    +
    
    313
    +
    
    314
    +if __name__ == "__main__":
    
    315
    +    parser = argparse.ArgumentParser()
    
    316
    +    parser.add_argument("issue_version")
    
    317
    +    parser.add_argument("-d", "--date", help="The date of the release")
    
    318
    +    parser.add_argument(
    
    319
    +        "-b", "--browser", choices=["tor-browser", "mullvad-browser"]
    
    320
    +    )
    
    321
    +    parser.add_argument(
    
    322
    +        "--firefox", help="New Firefox version (if we rebased)"
    
    323
    +    )
    
    324
    +    parser.add_argument("--tor", help="New Tor version (if updated)")
    
    325
    +    parser.add_argument(
    
    326
    +        "--noscript", "--no-script", help="New NoScript version (if updated)"
    
    327
    +    )
    
    328
    +    parser.add_argument("--openssl", help="New OpenSSL version (if updated)")
    
    329
    +    parser.add_argument("--zlib", help="New zlib version (if updated)")
    
    330
    +    parser.add_argument("--zstd", help="New zstd version (if updated)")
    
    331
    +    parser.add_argument("--go", help="New Go version (if updated)")
    
    332
    +    parser.add_argument(
    
    333
    +        "--mb-extension",
    
    334
    +        help="New Mullvad Browser Extension version (if updated)",
    
    335
    +    )
    
    336
    +    parser.add_argument("--ublock", help="New uBlock version (if updated)")
    
    337
    +    args = parser.parse_args()
    
    338
    +
    
    339
    +    if not args.issue_version:
    
    340
    +        parser.print_help()
    
    341
    +        sys.exit(1)
    
    342
    +
    
    343
    +    try:
    
    344
    +        token = load_token()
    
    345
    +    except ValueError:
    
    346
    +        print(
    
    347
    +            "Invalid authentication token. Maybe has it expired?",
    
    348
    +            file=sys.stderr,
    
    349
    +        )
    
    350
    +        sys.exit(2)
    
    351
    +    is_mullvad = args.browser == "mullvad-browser" if args.browser else None
    
    352
    +    cb = ChangelogBuilder(token, args.issue_version, is_mullvad)
    
    353
    +    print(
    
    354
    +        cb.create(
    
    355
    +            date=args.date,
    
    356
    +            firefox=args.firefox,
    
    357
    +            tor=args.tor,
    
    358
    +            noscript=args.noscript,
    
    359
    +            openssl=args.openssl,
    
    360
    +            zlib=args.zlib,
    
    361
    +            zstd=args.zstd,
    
    362
    +            go=args.go,
    
    363
    +            mb_extension=args.mb_extension,
    
    364
    +            ublock=args.ublock,
    
    365
    +        )
    
    366
    +    )

  • tools/relprep.py
    1
    +#!/usr/bin/env python3
    
    2
    +import argparse
    
    3
    +from collections import namedtuple
    
    4
    +import configparser
    
    5
    +from datetime import datetime, timezone
    
    6
    +from hashlib import sha256
    
    7
    +import json
    
    8
    +import locale
    
    9
    +import logging
    
    10
    +from pathlib import Path
    
    11
    +import re
    
    12
    +import sys
    
    13
    +import xml.etree.ElementTree as ET
    
    14
    +
    
    15
    +from git import Repo
    
    16
    +import requests
    
    17
    +import ruamel.yaml
    
    18
    +
    
    19
    +from fetch_allowed_addons import NOSCRIPT, fetch_allowed_addons, find_addon
    
    20
    +import fetch_changelogs
    
    21
    +from update_manual import update_manual
    
    22
    +
    
    23
    +
    
    24
    +logger = logging.getLogger(__name__)
    
    25
    +
    
    26
    +
    
    27
    +ReleaseTag = namedtuple("ReleaseTag", ["tag", "version"])
    
    28
    +
    
    29
    +
    
    30
    +class Version:
    
    31
    +    def __init__(self, v):
    
    32
    +        self.v = v
    
    33
    +        m = re.match(r"(\d+\.\d+)([a\.])?(\d*)", v)
    
    34
    +        self.major = m.group(1)
    
    35
    +        self.minor = int(m.group(3)) if m.group(3) else 0
    
    36
    +        self.is_alpha = m.group(2) == "a"
    
    37
    +        self.channel = "alpha" if self.is_alpha else "release"
    
    38
    +
    
    39
    +    def __str__(self):
    
    40
    +        return self.v
    
    41
    +
    
    42
    +    def __lt__(self, other):
    
    43
    +        if self.major != other.major:
    
    44
    +            # String comparison, but it should be fine until
    
    45
    +            # version 100 :)
    
    46
    +            return self.major < other.major
    
    47
    +        if self.is_alpha != other.is_alpha:
    
    48
    +            return self.is_alpha
    
    49
    +        # Same major, both alphas/releases
    
    50
    +        return self.minor < other.minor
    
    51
    +
    
    52
    +    def __eq__(self, other):
    
    53
    +        return self.v == other.v
    
    54
    +
    
    55
    +    def __hash__(self):
    
    56
    +        return hash(self.v)
    
    57
    +
    
    58
    +
    
    59
    +def get_sorted_tags(repo):
    
    60
    +    return sorted(
    
    61
    +        [t.tag for t in repo.tags if t.tag],
    
    62
    +        key=lambda t: t.tagged_date,
    
    63
    +        reverse=True,
    
    64
    +    )
    
    65
    +
    
    66
    +
    
    67
    +def get_github_release(project, regex=""):
    
    68
    +    if regex:
    
    69
    +        regex = re.compile(regex)
    
    70
    +    url = f"https://github.com/{project}/releases.atom"
    
    71
    +    r = requests.get(url)
    
    72
    +    r.raise_for_status()
    
    73
    +    feed = ET.fromstring(r.text)
    
    74
    +    for entry in feed.findall("{http://www.w3.org/2005/Atom}entry"):
    
    75
    +        link = entry.find("{http://www.w3.org/2005/Atom}link").attrib["href"]
    
    76
    +        tag = link.split("/")[-1]
    
    77
    +        if regex:
    
    78
    +            m = regex.match(tag)
    
    79
    +            if m:
    
    80
    +                return m.group(1)
    
    81
    +        else:
    
    82
    +            return tag
    
    83
    +
    
    84
    +
    
    85
    +class ReleasePreparation:
    
    86
    +    def __init__(self, repo_path, version, **kwargs):
    
    87
    +        logger.debug(
    
    88
    +            "Initializing. Version=%s, repo=%s, additional args=%s",
    
    89
    +            repo_path,
    
    90
    +            version,
    
    91
    +            kwargs,
    
    92
    +        )
    
    93
    +        self.base_path = Path(repo_path)
    
    94
    +        self.repo = Repo(self.base_path)
    
    95
    +
    
    96
    +        self.tor_browser = bool(kwargs.get("tor_browser", True))
    
    97
    +        self.mullvad_browser = bool(kwargs.get("tor_browser", True))
    
    98
    +        if not self.tor_browser and not self.mullvad_browser:
    
    99
    +            raise ValueError("Nothing to do")
    
    100
    +        self.android = kwargs.get("android", self.tor_browser)
    
    101
    +        if not self.tor_browser and self.android:
    
    102
    +            raise ValueError("Only Tor Browser supports Android")
    
    103
    +
    
    104
    +        logger.debug(
    
    105
    +            "Tor Browser: %s; Mullvad Browser: %s; Android: %s",
    
    106
    +            self.tor_browser,
    
    107
    +            self.mullvad_browser,
    
    108
    +            self.android,
    
    109
    +        )
    
    110
    +
    
    111
    +        self.yaml = ruamel.yaml.YAML()
    
    112
    +        self.yaml.indent(mapping=2, sequence=4, offset=2)
    
    113
    +        self.yaml.width = 4096
    
    114
    +        self.yaml.preserve_quotes = True
    
    115
    +
    
    116
    +        self.version = Version(version)
    
    117
    +
    
    118
    +        self.build_date = kwargs.get("build_date", datetime.now(timezone.utc))
    
    119
    +        self.changelog_date = kwargs.get("changelog_date", self.build_date)
    
    120
    +        self.num_incrementals = kwargs.get("num_incrementals", 3)
    
    121
    +
    
    122
    +        self.get_last_releases()
    
    123
    +
    
    124
    +        logger.info("Checking you have a working GitLab token.")
    
    125
    +        self.gitlab_token = fetch_changelogs.load_token()
    
    126
    +
    
    127
    +    def run(self):
    
    128
    +        self.branch_sanity_check()
    
    129
    +
    
    130
    +        self.update_firefox()
    
    131
    +        if self.android:
    
    132
    +            self.update_firefox_android()
    
    133
    +        self.update_translations()
    
    134
    +        self.update_addons()
    
    135
    +
    
    136
    +        if self.tor_browser:
    
    137
    +            self.update_tor()
    
    138
    +            self.update_openssl()
    
    139
    +            self.update_zlib()
    
    140
    +            if self.android:
    
    141
    +                self.update_zstd()
    
    142
    +            self.update_go()
    
    143
    +            self.update_manual()
    
    144
    +
    
    145
    +        self.update_changelogs()
    
    146
    +        self.update_rbm_conf()
    
    147
    +
    
    148
    +        logger.info("Release preparation complete!")
    
    149
    +
    
    150
    +    def branch_sanity_check(self):
    
    151
    +        logger.info("Checking you are on an updated branch.")
    
    152
    +
    
    153
    +        remote = None
    
    154
    +        for rem in self.repo.remotes:
    
    155
    +            if "tpo/applications/tor-browser-build" in rem.url:
    
    156
    +                remote = rem
    
    157
    +                break
    
    158
    +        if remote is None:
    
    159
    +            raise RuntimeError("Cannot find the tpo/applications remote.")
    
    160
    +        remote.fetch()
    
    161
    +
    
    162
    +        branch_name = (
    
    163
    +            "main" if self.version.is_alpha else f"maint-{self.version.major}"
    
    164
    +        )
    
    165
    +        branch = remote.refs[branch_name]
    
    166
    +        base = self.repo.merge_base(self.repo.head, branch)[0]
    
    167
    +        if base != branch.commit:
    
    168
    +            raise RuntimeError(
    
    169
    +                "You are not working on a branch descending from "
    
    170
    +                f"f{branch_name}. "
    
    171
    +                "Please checkout the correct branch, or pull/rebase."
    
    172
    +            )
    
    173
    +        logger.debug("Sanity check succeeded.")
    
    174
    +
    
    175
    +    def update_firefox(self):
    
    176
    +        logger.info("Updating Firefox (and GeckoView if needed)")
    
    177
    +        config = self.load_config("firefox")
    
    178
    +
    
    179
    +        tag_tb = None
    
    180
    +        tag_mb = None
    
    181
    +        if self.tor_browser:
    
    182
    +            tag_tb = self._get_firefox_tag(config, "tor-browser")
    
    183
    +            logger.debug(
    
    184
    +                "Tor Browser tag: ff=%s, rebase=%s, build=%s",
    
    185
    +                tag_tb[0],
    
    186
    +                tag_tb[1],
    
    187
    +                tag_tb[2],
    
    188
    +            )
    
    189
    +        if self.mullvad_browser:
    
    190
    +            tag_mb = self._get_firefox_tag(config, "mullvad-browser")
    
    191
    +            logger.debug(
    
    192
    +                "Mullvad Browser tag: ff=%s, rebase=%s, build=%s",
    
    193
    +                tag_mb[0],
    
    194
    +                tag_mb[1],
    
    195
    +                tag_mb[2],
    
    196
    +            )
    
    197
    +        if (
    
    198
    +            tag_mb
    
    199
    +            and (not tag_tb or tag_tb[2] == tag_mb[2])
    
    200
    +            and "browser_build" in config["targets"]["mullvadbrowser"]["var"]
    
    201
    +        ):
    
    202
    +            logger.debug(
    
    203
    +                "Tor Browser and Mullvad Browser are on the same build number, deleting unnecessary targets/mullvadbrowser/var/browser_build."
    
    204
    +            )
    
    205
    +            del config["targets"]["mullvadbrowser"]["var"]["browser_build"]
    
    206
    +        elif tag_mb and tag_tb and tag_mb[2] != tag_tb[2]:
    
    207
    +            config["targets"]["mullvadbrowser"]["var"]["browser_build"] = (
    
    208
    +                tag_mb[2]
    
    209
    +            )
    
    210
    +            logger.debug(
    
    211
    +                "Mismatching builds for TBB and MB, will add targets/mullvadbrowser/var/browser_build."
    
    212
    +            )
    
    213
    +        # We assume firefox version and rebase to be in sync
    
    214
    +        if tag_tb:
    
    215
    +            version = tag_tb[0]
    
    216
    +            rebase = tag_tb[1]
    
    217
    +            build = tag_tb[2]
    
    218
    +        elif tag_mb:
    
    219
    +            version = tag_mb[0]
    
    220
    +            rebase = tag_mb[1]
    
    221
    +            build = tag_mb[2]
    
    222
    +        platform = version[:-3] if version.endswith("esr") else version
    
    223
    +        config["var"]["firefox_platform_version"] = platform
    
    224
    +        config["var"]["browser_rebase"] = rebase
    
    225
    +        config["var"]["browser_build"] = build
    
    226
    +        self.save_config("firefox", config)
    
    227
    +        logger.debug("Firefox configuration saved")
    
    228
    +
    
    229
    +        if self.android:
    
    230
    +            assert tag_tb
    
    231
    +            config = self.load_config("geckoview")
    
    232
    +            config["var"]["geckoview_version"] = tag_tb[0]
    
    233
    +            config["var"][
    
    234
    +                "browser_branch"
    
    235
    +            ] = f"{self.version.major}-{tag_tb[1]}"
    
    236
    +            config["var"]["browser_build"] = tag_tb[2]
    
    237
    +            self.save_config("geckoview", config)
    
    238
    +            logger.debug("GeckoView configuration saved")
    
    239
    +
    
    240
    +    def _get_firefox_tag(self, config, browser):
    
    241
    +        if browser == "mullvad-browser":
    
    242
    +            remote = config["targets"]["mullvadbrowser"]["git_url"]
    
    243
    +        else:
    
    244
    +            remote = config["git_url"]
    
    245
    +        repo = Repo(self.base_path / "git_clones/firefox")
    
    246
    +        repo.remotes["origin"].set_url(remote)
    
    247
    +        logger.debug("About to fetch Firefox from %s.", remote)
    
    248
    +        repo.remotes["origin"].fetch()
    
    249
    +        tags = get_sorted_tags(repo)
    
    250
    +        for t in tags:
    
    251
    +            m = re.match(
    
    252
    +                r"(\w+-browser)-([^-]+)-([\d\.]+)-(\d+)-build(\d+)", t.tag
    
    253
    +            )
    
    254
    +            if (
    
    255
    +                m
    
    256
    +                and m.group(1) == browser
    
    257
    +                and m.group(3) == self.version.major
    
    258
    +            ):
    
    259
    +                # firefox-version, rebase, build
    
    260
    +                return (m.group(2), int(m.group(4)), int(m.group(5)))
    
    261
    +
    
    262
    +    def update_firefox_android(self):
    
    263
    +        logger.info("Updating firefox-android")
    
    264
    +        config = self.load_config("firefox-android")
    
    265
    +        repo = Repo(self.base_path / "git_clones/firefox-android")
    
    266
    +        repo.remotes["origin"].fetch()
    
    267
    +        tags = get_sorted_tags(repo)
    
    268
    +        for t in tags:
    
    269
    +            m = re.match(
    
    270
    +                r"firefox-android-([^-]+)-([\d\.]+)-(\d+)-build(\d+)", t.tag
    
    271
    +            )
    
    272
    +            if not m or m.group(2) != self.version.major:
    
    273
    +                logger.debug("Discarding firefox-android tag: %s", t.tag)
    
    274
    +                continue
    
    275
    +            logger.debug("Using firefox-android tag: %s", t.tag)
    
    276
    +            config["var"]["fenix_version"] = m.group(1)
    
    277
    +            config["var"]["browser_branch"] = m.group(2) + "-" + m.group(3)
    
    278
    +            config["var"]["browser_build"] = int(m.group(4))
    
    279
    +            break
    
    280
    +        self.save_config("firefox-android", config)
    
    281
    +
    
    282
    +    def update_translations(self):
    
    283
    +        logger.info("Updating translations")
    
    284
    +        repo = Repo(self.base_path / "git_clones/translation")
    
    285
    +        repo.remotes["origin"].fetch()
    
    286
    +        config = self.load_config("translation")
    
    287
    +        targets = ["base-browser"]
    
    288
    +        if self.tor_browser:
    
    289
    +            targets.append("tor-browser")
    
    290
    +            targets.append("fenix")
    
    291
    +        if self.mullvad_browser:
    
    292
    +            targets.append("mullvad-browser")
    
    293
    +        for i in targets:
    
    294
    +            branch = config["steps"][i]["targets"]["nightly"]["git_hash"]
    
    295
    +            config["steps"][i]["git_hash"] = str(
    
    296
    +                repo.rev_parse(f"origin/{branch}")
    
    297
    +            )
    
    298
    +        self.save_config("translation", config)
    
    299
    +        logger.debug("Translations updated")
    
    300
    +
    
    301
    +    def update_addons(self):
    
    302
    +        logger.info("Updating addons")
    
    303
    +        config = self.load_config("browser")
    
    304
    +
    
    305
    +        amo_data = fetch_allowed_addons()
    
    306
    +        logger.debug("Fetched AMO data")
    
    307
    +        if self.android:
    
    308
    +            with (
    
    309
    +                self.base_path / "projects/browser/allowed_addons.json"
    
    310
    +            ).open("w") as f:
    
    311
    +                json.dump(amo_data, f, indent=2)
    
    312
    +
    
    313
    +        noscript = find_addon(amo_data, NOSCRIPT)
    
    314
    +        logger.debug("Updating NoScript")
    
    315
    +        self.update_addon_amo(config, "noscript", noscript)
    
    316
    +        if self.mullvad_browser:
    
    317
    +            logger.debug("Updating uBlock Origin")
    
    318
    +            ublock = find_addon(amo_data, "uBlock0@xxxxxxxxxxxxxxx")
    
    319
    +            self.update_addon_amo(config, "ublock-origin", ublock)
    
    320
    +            logger.debug("Updating the Mullvad Browser extension")
    
    321
    +            self.update_mullvad_addon(config)
    
    322
    +
    
    323
    +        self.save_config("browser", config)
    
    324
    +
    
    325
    +    def update_addon_amo(self, config, name, addon):
    
    326
    +        addon = addon["current_version"]["files"][0]
    
    327
    +        assert addon["hash"].startswith("sha256:")
    
    328
    +        addon_input = self.find_input(config, name)
    
    329
    +        addon_input["URL"] = addon["url"]
    
    330
    +        addon_input["sha256sum"] = addon["hash"][7:]
    
    331
    +
    
    332
    +    def update_mullvad_addon(self, config):
    
    333
    +        input_ = self.find_input(config, "mullvad-extension")
    
    334
    +        r = requests.get(
    
    335
    +            "https://cdn.mullvad.net/browser-extension/updates.json"
    
    336
    +        )
    
    337
    +        r.raise_for_status()
    
    338
    +
    
    339
    +        data = r.json()
    
    340
    +        updates = data["addons"]["{d19a89b9-76c1-4a61-bcd4-49e8de916403}"][
    
    341
    +            "updates"
    
    342
    +        ]
    
    343
    +        url = updates[-1]["update_link"]
    
    344
    +        if input_["URL"] == url:
    
    345
    +            logger.debug("No need to update the Mullvad extension.")
    
    346
    +            return
    
    347
    +        input_["URL"] = url
    
    348
    +
    
    349
    +        path = self.base_path / "out/browser" / url.split("/")[-1]
    
    350
    +        # The extension should be small enough to easily fit in memory :)
    
    351
    +        if not path.exists:
    
    352
    +            r = requests.get(url)
    
    353
    +            r.raise_for_status()
    
    354
    +            with path.open("wb") as f:
    
    355
    +                f.write(r.bytes)
    
    356
    +        with path.open("rb") as f:
    
    357
    +            input_["sha256sum"] = sha256(f.read()).hexdigest()
    
    358
    +        logger.debug("Mullvad extension downloaded and updated")
    
    359
    +
    
    360
    +    def update_tor(self):
    
    361
    +        logger.info("Updating Tor")
    
    362
    +        databag = configparser.ConfigParser()
    
    363
    +        r = requests.get(
    
    364
    +            "https://gitlab.torproject.org/tpo/web/tpo/-/raw/main/databags/versions.ini"
    
    365
    +        )
    
    366
    +        r.raise_for_status()
    
    367
    +        databag.read_string(r.text)
    
    368
    +        tor_stable = databag["tor-stable"]["version"]
    
    369
    +        tor_alpha = databag["tor-alpha"]["version"]
    
    370
    +        logger.debug(
    
    371
    +            "Found tor stable: %s, alpha: %s",
    
    372
    +            tor_stable,
    
    373
    +            tor_alpha if tor_alpha else "(empty)",
    
    374
    +        )
    
    375
    +        if self.version.is_alpha and tor_alpha:
    
    376
    +            version = tor_alpha
    
    377
    +        else:
    
    378
    +            version = tor_stable
    
    379
    +
    
    380
    +        config = self.load_config("tor")
    
    381
    +        if version != config["version"]:
    
    382
    +            config["version"] = version
    
    383
    +            self.save_config("tor", config)
    
    384
    +            logger.debug("Tor updated to %s and config saved", version)
    
    385
    +        else:
    
    386
    +            logger.debug(
    
    387
    +                "No need to update Tor (current version: %s).", version
    
    388
    +            )
    
    389
    +
    
    390
    +    def update_openssl(self):
    
    391
    +        logger.info("Updating OpenSSL")
    
    392
    +        config = self.load_config("openssl")
    
    393
    +        version = get_github_release("openssl/openssl", r"openssl-(3.0.\d+)")
    
    394
    +        if version == config["version"]:
    
    395
    +            logger.debug("No need to update OpenSSL, keeping %s.", version)
    
    396
    +            return
    
    397
    +
    
    398
    +        config["version"] = version
    
    399
    +
    
    400
    +        source = self.find_input(config, "openssl")
    
    401
    +        # No need to update URL, as it uses a variable.
    
    402
    +        hash_url = (
    
    403
    +            f"https://www.openssl.org/source/openssl-{version}.tar.gz.sha256"
    
    404
    +        )
    
    405
    +        r = requests.get(hash_url)
    
    406
    +        r.raise_for_status()
    
    407
    +        source["sha256sum"] = r.text.strip()
    
    408
    +        self.save_config("openssl", config)
    
    409
    +        logger.debug("Updated OpenSSL to %s and config saved.", version)
    
    410
    +
    
    411
    +    def update_zlib(self):
    
    412
    +        logger.info("Updating zlib")
    
    413
    +        config = self.load_config("zlib")
    
    414
    +        version = get_github_release("madler/zlib", r"v([0-9\.]+)")
    
    415
    +        if version == config["version"]:
    
    416
    +            logger.debug("No need to update zlib, keeping %s.", version)
    
    417
    +            return
    
    418
    +        config["version"] = version
    
    419
    +        self.save_config("zlib", config)
    
    420
    +        logger.debug("Updated zlib to %s and config saved.", version)
    
    421
    +
    
    422
    +    def update_zstd(self):
    
    423
    +        logger.info("Updating Zstandard")
    
    424
    +        config = self.load_config("zstd")
    
    425
    +        version = get_github_release("facebook/zstd", r"v([0-9\.]+)")
    
    426
    +        if version == config["version"]:
    
    427
    +            logger.debug("No need to update Zstandard, keeping %s.", version)
    
    428
    +            return
    
    429
    +
    
    430
    +        repo = Repo(self.base_path / "git_clones/zstd")
    
    431
    +        repo.remotes["origin"].fetch()
    
    432
    +        tag = repo.rev_parse(f"v{version}")
    
    433
    +
    
    434
    +        config["version"] = version
    
    435
    +        config["git_hash"] = tag.object.hexsha
    
    436
    +        self.save_config("zstd", config)
    
    437
    +        logger.debug(
    
    438
    +            "Updated Zstandard to %s (commit %s) and config saved.",
    
    439
    +            version,
    
    440
    +            config["git_hash"],
    
    441
    +        )
    
    442
    +
    
    443
    +    def update_go(self):
    
    444
    +        def get_major(v):
    
    445
    +            major = ".".join(v.split(".")[:2])
    
    446
    +            if major.startswith("go"):
    
    447
    +                major = major[2:]
    
    448
    +            return major
    
    449
    +
    
    450
    +        config = self.load_config("go")
    
    451
    +        # TODO: When Windows 7 goes EOL use config["version"]
    
    452
    +        major = get_major(config["var"]["go_1_21"])
    
    453
    +
    
    454
    +        r = requests.get("https://go.dev/dl/?mode=json")
    
    455
    +        r.raise_for_status()
    
    456
    +        go_versions = r.json()
    
    457
    +        data = None
    
    458
    +        for v in go_versions:
    
    459
    +            if get_major(v["version"]) == major:
    
    460
    +                data = v
    
    461
    +                break
    
    462
    +        if not data:
    
    463
    +            raise KeyError("Could not find information for our Go series.")
    
    464
    +        # Skip the "go" prefix in the version.
    
    465
    +        config["var"]["go_1_21"] = data["version"][2:]
    
    466
    +
    
    467
    +        sha256sum = ""
    
    468
    +        for f in data["files"]:
    
    469
    +            if f["kind"] == "source":
    
    470
    +                sha256sum = f["sha256"]
    
    471
    +                break
    
    472
    +        if not sha256sum:
    
    473
    +            raise KeyError("Go source package not found.")
    
    474
    +        updated_hash = False
    
    475
    +        for input_ in config["input_files"]:
    
    476
    +            if "URL" in input_ and "var/go_1_21" in input_["URL"]:
    
    477
    +                input_["sha256sum"] = sha256sum
    
    478
    +                updated_hash = True
    
    479
    +                break
    
    480
    +        if not updated_hash:
    
    481
    +            raise KeyError("Could not find a matching entry in input_files.")
    
    482
    +
    
    483
    +        self.save_config("go", config)
    
    484
    +
    
    485
    +    def update_manual(self):
    
    486
    +        logger.info("Updating the manual")
    
    487
    +        update_manual(self.gitlab_token, self.base_path)
    
    488
    +
    
    489
    +    def get_last_releases(self):
    
    490
    +        logger.info("Finding the previous releases.")
    
    491
    +        sorted_tags = get_sorted_tags(self.repo)
    
    492
    +        self.last_releases = {}
    
    493
    +        self.build_number = 1
    
    494
    +        regex = re.compile(r"(\w+)-([\d\.a]+)-build(\d+)")
    
    495
    +        num_releases = 0
    
    496
    +        for t in sorted_tags:
    
    497
    +            m = regex.match(t.tag)
    
    498
    +            project = m.group(1)
    
    499
    +            version = Version(m.group(2))
    
    500
    +            build = int(m.group(3))
    
    501
    +            if version == self.version:
    
    502
    +                # A previous tag, we can use it to bump our build.
    
    503
    +                if self.build_number == 1:
    
    504
    +                    self.build_number = build + 1
    
    505
    +                    logger.debug(
    
    506
    +                        "Found previous tag for the version we are preparing: %s. Bumping build number to %d.",
    
    507
    +                        t.tag,
    
    508
    +                        self.build_number,
    
    509
    +                    )
    
    510
    +                continue
    
    511
    +            key = (project, version.channel)
    
    512
    +            if key not in self.last_releases:
    
    513
    +                self.last_releases[key] = []
    
    514
    +            skip = False
    
    515
    +            for rel in self.last_releases[key]:
    
    516
    +                # Tags are already sorted: higher builds should come
    
    517
    +                # first.
    
    518
    +                if rel.version == version:
    
    519
    +                    skip = True
    
    520
    +                    logger.debug(
    
    521
    +                        "Additional build for a version we already found, skipping: %s",
    
    522
    +                        t.tag,
    
    523
    +                    )
    
    524
    +                    break
    
    525
    +            if skip:
    
    526
    +                continue
    
    527
    +            if len(self.last_releases[key]) != self.num_incrementals:
    
    528
    +                logger.debug(
    
    529
    +                    "Found tag to potentially build incrementals from: %s.",
    
    530
    +                    t.tag,
    
    531
    +                )
    
    532
    +                self.last_releases[key].append(ReleaseTag(t, version))
    
    533
    +                num_releases += 1
    
    534
    +            if num_releases == self.num_incrementals * 4:
    
    535
    +                break
    
    536
    +
    
    537
    +    def update_changelogs(self):
    
    538
    +        if self.tor_browser:
    
    539
    +            logger.info("Updating changelogs for Tor Browser")
    
    540
    +            self.make_changelogs("tbb")
    
    541
    +        if self.mullvad_browser:
    
    542
    +            logger.info("Updating changelogs for Mullvad Browser")
    
    543
    +            self.make_changelogs("mb")
    
    544
    +
    
    545
    +    def make_changelogs(self, tag_prefix):
    
    546
    +        locale.setlocale(locale.LC_TIME, "C")
    
    547
    +        kwargs = {"date": self.changelog_date.strftime("%B %d %Y")}
    
    548
    +        prev_tag = self.last_releases[(tag_prefix, self.version.channel)][
    
    549
    +            0
    
    550
    +        ].tag
    
    551
    +        self.check_update(
    
    552
    +            kwargs, prev_tag, "firefox", ["var", "firefox_platform_version"]
    
    553
    +        )
    
    554
    +        if "firefox" in kwargs:
    
    555
    +            # Sometimes this might be incorrect for alphas, but let's
    
    556
    +            # keep it for now.
    
    557
    +            kwargs["firefox"] += "esr"
    
    558
    +        self.check_update_simple(kwargs, prev_tag, "tor")
    
    559
    +        self.check_update_simple(kwargs, prev_tag, "openssl")
    
    560
    +        self.check_update_simple(kwargs, prev_tag, "zlib")
    
    561
    +        self.check_update_simple(kwargs, prev_tag, "zstd")
    
    562
    +        try:
    
    563
    +            self.check_update(kwargs, prev_tag, "go", ["var", "go_1_21"])
    
    564
    +        except KeyError as e:
    
    565
    +            logger.warning(
    
    566
    +                "Go: var/go_1_21 not found, marking Go as not updated.",
    
    567
    +                exc_info=e,
    
    568
    +            )
    
    569
    +            pass
    
    570
    +        self.check_update_extensions(kwargs, prev_tag)
    
    571
    +        logger.debug("Changelog arguments for %s: %s", tag_prefix, kwargs)
    
    572
    +        cb = fetch_changelogs.ChangelogBuilder(
    
    573
    +            self.gitlab_token, str(self.version), is_mullvad=tag_prefix == "mb"
    
    574
    +        )
    
    575
    +        changelogs = cb.create(**kwargs)
    
    576
    +
    
    577
    +        path = f"projects/browser/Bundle-Data/Docs-{tag_prefix.upper()}/ChangeLog.txt"
    
    578
    +        stable_tag = self.last_releases[(tag_prefix, "release")][0].tag
    
    579
    +        alpha_tag = self.last_releases[(tag_prefix, "alpha")][0].tag
    
    580
    +        if stable_tag.tagged_date > alpha_tag.tagged_date:
    
    581
    +            last_tag = stable_tag
    
    582
    +        else:
    
    583
    +            last_tag = alpha_tag
    
    584
    +        logger.debug("Using %s to add the new changelogs to.", last_tag.tag)
    
    585
    +        last_changelogs = self.repo.git.show(f"{last_tag.tag}:{path}")
    
    586
    +        with (self.base_path / path).open("w") as f:
    
    587
    +            f.write(changelogs + "\n" + last_changelogs + "\n")
    
    588
    +
    
    589
    +    def check_update(self, updates, prev_tag, project, key):
    
    590
    +        old_val = self.load_old_config(prev_tag.tag, project)
    
    591
    +        new_val = self.load_config(project)
    
    592
    +        for k in key:
    
    593
    +            old_val = old_val[k]
    
    594
    +            new_val = new_val[k]
    
    595
    +        if old_val != new_val:
    
    596
    +            updates[project] = new_val
    
    597
    +
    
    598
    +    def check_update_simple(self, updates, prev_tag, project):
    
    599
    +        self.check_update(updates, prev_tag, project, ["version"])
    
    600
    +
    
    601
    +    def check_update_extensions(self, updates, prev_tag):
    
    602
    +        old_config = self.load_old_config(prev_tag, "browser")
    
    603
    +        new_config = self.load_config("browser")
    
    604
    +        keys = {
    
    605
    +            "noscript": "noscript",
    
    606
    +            "mb_extension": "mullvad-extension",
    
    607
    +            "ublock": "ublock-origin",
    
    608
    +        }
    
    609
    +        regex = re.compile(r"-([0-9\.]+).xpi$")
    
    610
    +        for update_key, input_name in keys.items():
    
    611
    +            old_url = self.find_input(old_config, input_name)["URL"]
    
    612
    +            new_url = self.find_input(new_config, input_name)["URL"]
    
    613
    +            old_version = regex.findall(old_url)[0]
    
    614
    +            new_version = regex.findall(new_url)[0]
    
    615
    +            if old_version != new_version:
    
    616
    +                updates[update_key] = new_version
    
    617
    +
    
    618
    +    def update_rbm_conf(self):
    
    619
    +        logger.info("Updating rbm.conf.")
    
    620
    +        releases = {}
    
    621
    +        browsers = {
    
    622
    +            "tbb": '[% IF c("var/tor-browser") %]{}[% END %]',
    
    623
    +            "mb": '[% IF c("var/mullvad-browser") %]{}[% END %]',
    
    624
    +        }
    
    625
    +        incremental_from = []
    
    626
    +        for b in ["tbb", "mb"]:
    
    627
    +            for rel in self.last_releases[(b, self.version.channel)]:
    
    628
    +                if rel.version not in releases:
    
    629
    +                    releases[rel.version] = {}
    
    630
    +                releases[rel.version][b] = str(rel.version)
    
    631
    +        for version in sorted(releases.keys(), reverse=True):
    
    632
    +            if len(releases[version]) == 2:
    
    633
    +                incremental_from.append(releases[version]["tbb"])
    
    634
    +                logger.debug(
    
    635
    +                    "Building incremental from %s for both browsers.", version
    
    636
    +                )
    
    637
    +            else:
    
    638
    +                for b, template in browsers.items():
    
    639
    +                    maybe_rel = releases[version].get(b)
    
    640
    +                    if maybe_rel:
    
    641
    +                        logger.debug(
    
    642
    +                            "Building incremental from %s only for %s.",
    
    643
    +                            version,
    
    644
    +                            b,
    
    645
    +                        )
    
    646
    +                        incremental_from.append(template.format(maybe_rel))
    
    647
    +
    
    648
    +        separator = "\n--- |\n"
    
    649
    +        path = self.base_path / "rbm.conf"
    
    650
    +        with path.open() as f:
    
    651
    +            docs = f.read().split(separator, 2)
    
    652
    +        config = self.yaml.load(docs[0])
    
    653
    +        config["var"]["torbrowser_version"] = str(self.version)
    
    654
    +        config["var"]["torbrowser_build"] = f"build{self.build_number}"
    
    655
    +        config["var"]["torbrowser_incremental_from"] = incremental_from
    
    656
    +        config["var"]["browser_release_date"] = self.build_date.strftime(
    
    657
    +            "%Y/%m/%d %H:%M:%S"
    
    658
    +        )
    
    659
    +        with path.open("w") as f:
    
    660
    +            self.yaml.dump(config, f)
    
    661
    +            f.write(separator)
    
    662
    +            f.write(docs[1])
    
    663
    +
    
    664
    +    def load_config(self, project):
    
    665
    +        config_path = self.base_path / f"projects/{project}/config"
    
    666
    +        return self.yaml.load(config_path)
    
    667
    +
    
    668
    +    def load_old_config(self, committish, project):
    
    669
    +        treeish = f"{committish}:projects/{project}/config"
    
    670
    +        return self.yaml.load(self.repo.git.show(treeish))
    
    671
    +
    
    672
    +    def save_config(self, project, config):
    
    673
    +        config_path = self.base_path / f"projects/{project}/config"
    
    674
    +        with config_path.open("w") as f:
    
    675
    +            self.yaml.dump(config, f)
    
    676
    +
    
    677
    +    def find_input(self, config, name):
    
    678
    +        for entry in config["input_files"]:
    
    679
    +            if "name" in entry and entry["name"] == name:
    
    680
    +                return entry
    
    681
    +        raise KeyError(f"Input {name} not found.")
    
    682
    +
    
    683
    +
    
    684
    +if __name__ == "__main__":
    
    685
    +    parser = argparse.ArgumentParser()
    
    686
    +    parser.add_argument(
    
    687
    +        "-r",
    
    688
    +        "--repository",
    
    689
    +        type=Path,
    
    690
    +        default=Path(__file__).parent.parent,
    
    691
    +        help="Path to a tor-browser-build.git clone",
    
    692
    +    )
    
    693
    +    parser.add_argument("--tor-browser", action="store_true")
    
    694
    +    parser.add_argument("--mullvad-browser", action="store_true")
    
    695
    +    parser.add_argument(
    
    696
    +        "--date",
    
    697
    +        help="Release date and optionally time for changelog purposes. "
    
    698
    +        "It must be understandable by datetime.fromisoformat.",
    
    699
    +    )
    
    700
    +    parser.add_argument(
    
    701
    +        "--build-date",
    
    702
    +        help="Build date. It cannot not be in the future when running the build.",
    
    703
    +    )
    
    704
    +    parser.add_argument(
    
    705
    +        "--incrementals", type=int, help="The number of incrementals to create"
    
    706
    +    )
    
    707
    +    parser.add_argument(
    
    708
    +        "--only-changelogs",
    
    709
    +        action="store_true",
    
    710
    +        help="Only update the changelogs",
    
    711
    +    )
    
    712
    +    parser.add_argument(
    
    713
    +        "--log-level",
    
    714
    +        choices=["debug", "info", "warning", "error"],
    
    715
    +        default="info",
    
    716
    +        help="Set the log level",
    
    717
    +    )
    
    718
    +    parser.add_argument("version")
    
    719
    +
    
    720
    +    args = parser.parse_args()
    
    721
    +
    
    722
    +    # Logger adapted from https://stackoverflow.com/a/56944256.
    
    723
    +    log_level = getattr(logging, args.log_level.upper())
    
    724
    +    logger.setLevel(log_level)
    
    725
    +    ch = logging.StreamHandler()
    
    726
    +    ch.setLevel(log_level)
    
    727
    +    ch.setFormatter(
    
    728
    +        logging.Formatter(
    
    729
    +            "%(asctime)s - %(name)s - %(levelname)s - %(message)s (%(filename)s:%(lineno)d)",
    
    730
    +            datefmt="%Y-%m-%d %H:%M:%S",
    
    731
    +        )
    
    732
    +    )
    
    733
    +    logger.addHandler(ch)
    
    734
    +
    
    735
    +    tbb = bool(args.tor_browser)
    
    736
    +    mb = bool(args.mullvad_browser)
    
    737
    +    kwargs = {}
    
    738
    +    if tbb or mb:
    
    739
    +        kwargs["tor_browser"] = tbb
    
    740
    +        kwargs["mullvad_browser"] = mb
    
    741
    +    if args.date:
    
    742
    +        try:
    
    743
    +            kwargs["changelog_date"] = datetime.fromisoformat(args.date)
    
    744
    +        except ValueError:
    
    745
    +            print("Invalid date supplied.", file=sys.stderr)
    
    746
    +            sys.exit(1)
    
    747
    +    if args.build_date:
    
    748
    +        try:
    
    749
    +            kwargs["build_date"] = datetime.fromisoformat(args.date)
    
    750
    +        except ValueError:
    
    751
    +            print("Invalid date supplied.", file=sys.stderr)
    
    752
    +            sys.exit(1)
    
    753
    +    if args.incrementals:
    
    754
    +        kwargs["incrementals"] = args.incrementals
    
    755
    +    rp = ReleasePreparation(args.repository, args.version, **kwargs)
    
    756
    +    if args.only_changelogs:
    
    757
    +        logger.info("Updating only the changelogs")
    
    758
    +        rp.update_changelogs()
    
    759
    +    else:
    
    760
    +        logger.debug("Running a complete release preparation.")
    
    761
    +        rp.run()

  • tools/update_manual.py
    1
    +#!/usr/bin/env python3
    
    2
    +import hashlib
    
    3
    +from pathlib import Path
    
    4
    +
    
    5
    +import requests
    
    6
    +import ruamel.yaml
    
    7
    +
    
    8
    +from fetch_changelogs import load_token, AUTH_HEADER
    
    9
    +
    
    10
    +
    
    11
    +GITLAB = "https://gitlab.torproject.org"
    
    12
    +API_URL = f"{GITLAB}/api/v4"
    
    13
    +PROJECT_ID = 23
    
    14
    +REF_NAME = "main"
    
    15
    +
    
    16
    +
    
    17
    +def find_job(auth_token):
    
    18
    +    r = requests.get(
    
    19
    +        f"{API_URL}/projects/{PROJECT_ID}/jobs",
    
    20
    +        headers={AUTH_HEADER: auth_token},
    
    21
    +    )
    
    22
    +    r.raise_for_status()
    
    23
    +    for job in r.json():
    
    24
    +        if job["ref"] != REF_NAME:
    
    25
    +            continue
    
    26
    +        for artifact in job["artifacts"]:
    
    27
    +            if artifact["filename"] == "artifacts.zip":
    
    28
    +                return job
    
    29
    +
    
    30
    +
    
    31
    +def update_config(base_path, pipeline_id, sha256):
    
    32
    +    yaml = ruamel.yaml.YAML()
    
    33
    +    yaml.indent(mapping=2, sequence=4, offset=2)
    
    34
    +    yaml.width = 150
    
    35
    +    yaml.preserve_quotes = True
    
    36
    +
    
    37
    +    config_path = base_path / "projects/manual/config"
    
    38
    +    config = yaml.load(config_path)
    
    39
    +    if int(config["version"]) == pipeline_id:
    
    40
    +        return False
    
    41
    +
    
    42
    +    config["version"] = pipeline_id
    
    43
    +    for input_file in config["input_files"]:
    
    44
    +        if input_file.get("name") == "manual":
    
    45
    +            input_file["sha256sum"] = sha256
    
    46
    +            break
    
    47
    +    with config_path.open("w") as f:
    
    48
    +        yaml.dump(config, f)
    
    49
    +    return True
    
    50
    +
    
    51
    +def download_manual(url, dest):
    
    52
    +    r = requests.get(url, stream=True)
    
    53
    +    # https://stackoverflow.com/a/16696317
    
    54
    +    r.raise_for_status()
    
    55
    +    sha256 = hashlib.sha256()
    
    56
    +    with dest.open("wb") as f:
    
    57
    +        for chunk in r.iter_content(chunk_size=8192):
    
    58
    +            f.write(chunk)
    
    59
    +            sha256.update(chunk)
    
    60
    +    return sha256.hexdigest()
    
    61
    +
    
    62
    +
    
    63
    +def update_manual(auth_token, base_path):
    
    64
    +    job = find_job(auth_token)
    
    65
    +    if job is None:
    
    66
    +        raise RuntimeError("No usable job found")
    
    67
    +    pipeline_id = int(job["pipeline"]["id"])
    
    68
    +
    
    69
    +    manual_fname = f"manual_{pipeline_id}.zip"
    
    70
    +    url = f"https://build-sources.tbb.torproject.org/{manual_fname}"
    
    71
    +    r = requests.head(url)
    
    72
    +    needs_upload = r.status_code != 200
    
    73
    +
    
    74
    +    manual_dir = base_path / "out/manual"
    
    75
    +    manual_dir.mkdir(0o755, parents=True, exist_ok=True)
    
    76
    +    manual_file = manual_dir / manual_fname
    
    77
    +    if manual_file.exists():
    
    78
    +        sha256 = hashlib.sha256()
    
    79
    +        with manual_file.open("rb") as f:
    
    80
    +            while chunk := f.read(8192):
    
    81
    +                sha256.update(chunk)
    
    82
    +        sha256 = sha256.hexdigest()
    
    83
    +    elif not needs_upload:
    
    84
    +        sha256 = download_manual(url, manual_file)
    
    85
    +    else:
    
    86
    +        url = f"{API_URL}/projects/{PROJECT_ID}/jobs/artifacts/{REF_NAME}/download?job={job['name']}"
    
    87
    +        sha256 = download_manual(url, manual_file)
    
    88
    +
    
    89
    +    if needs_upload:
    
    90
    +        print(f"New manual version: {manual_file}.")
    
    91
    +        print(
    
    92
    +            "Please upload it to tb-build-02.torproject.org:~tb-builder/public_html/."
    
    93
    +        )
    
    94
    +
    
    95
    +    return update_config(base_path, pipeline_id, sha256)
    
    96
    +
    
    97
    +
    
    98
    +if __name__ == "__main__":
    
    99
    +    if update_manual(load_token(), Path(__file__).parent.parent):
    
    100
    +        print("Manual config updated, remember to stage it!")

  • _______________________________________________
    tor-commits mailing list
    tor-commits@xxxxxxxxxxxxxxxxxxxx
    https://lists.torproject.org/cgi-bin/mailman/listinfo/tor-commits