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

Commits:

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@raymondhill.net")
    
    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!")