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