Merge branch 'main' into split-launchpad

This commit is contained in:
Erica Portnoy
2026-07-23 13:21:11 -07:00
31 changed files with 221 additions and 67 deletions
+2 -2
View File
@@ -1,8 +1,8 @@
## Pull Request Checklist
- [ ] The Certbot team has recently expressed interest in reviewing a PR for this. If not, this PR may be closed due our limited resources and need to prioritize how we spend them.
- [ ] If you used AI to create this PR, you have done a self-review of all AI-generated code and disclosed that your contribution was AI-generated per [EFF's AI-generated contribution policy](https://www.eff.org/about/opportunities/volunteer/coding-with-eff). You assert you have thoroughly understood, reviewed, and tested your entire submission.
- [ ] If you used AI to create this PR, you have done a self-review of all AI-generated code and disclosed that your contribution was AI-generated per [EFF's AI-generated contribution policy](https://www.eff.org/about/opportunities/volunteer/coding-with-eff#ai-policy). You assert you have thoroughly understood, reviewed, and tested your entire submission.
- [ ] If the change being made is to a [distributed component](https://certbot.eff.org/docs/contributing.html#code-components-and-layout), add a description of your change to the `newsfragments` directory. This should be a file called `<title>.<type>`, where `<title>` is either a GitHub issue number or some other unique name starting with `+`, and `<type>` is either `changed`, `fixed`, or `added`.
* For example, if you fixed a bug for issue number 42, create a file called `42.fixed` and put a description of your change in that file.
- [ ] Add or update any documentation as needed to support the changes in this PR.
- [ ] Include your name in `AUTHORS.md` if you like.
- [ ] Include your name in `AUTHORS.md` if you like.
-29
View File
@@ -1,29 +0,0 @@
name: Weekly Github Update
on:
schedule:
# Every week on Thursday @ 10:00
- cron: "0 10 * * 4"
workflow_dispatch:
permissions: {} # let's not use any permissions we don't need here
jobs:
send-mattermost-message:
runs-on: ubuntu-latest
steps:
- name: Create Mattermost Message
run: |
DATE=$(date --date="7 days ago" +"%Y-%m-%d")
echo "ASSIGNED_PRS=https://github.com/pulls?q=is%3Apr+is%3Aopen+updated%3A%3E%3D${DATE}+assignee%3A*+user%3Acertbot" >> $GITHUB_ENV
echo "UPDATED_URL=https://github.com/issues?q=is%3Aissue+is%3Aopen+sort%3Acomments-desc+updated%3A%3E%3D${DATE}+user%3Acertbot" >> $GITHUB_ENV
# we pin this action to a version tested and audited by certbot's
# maintainers for extra security. the full hash is used as doing so is
# recommended by zizmor
- uses: mattermost/action-mattermost-notify@ae31bb6f9e26a54336e79696f108a2c91cf55b4e
with:
MATTERMOST_WEBHOOK_URL: ${{ secrets.MATTERMOST_WEBHOOK_URL }}
MATTERMOST_CHANNEL: private-certbot
TEXT: |
## Updates In the Past Week
- Most commented in the last week: [link](${{ env.UPDATED_URL }})
- Updated (assigned) PRs in the last week: [link](${{ env.ASSIGNED_PRS }})
+1 -1
View File
@@ -46,7 +46,7 @@ jobs:
persist-credentials: false
- id: set-dns-dirs
run: |
DNS_NAMES="$(echo certbot-dns-* | jq -R -s -c 'split(" ")[:-1]')"
DNS_NAMES="$(echo certbot-dns-* | jq -R -s -c 'gsub("\n"; "") | split(" ")')"
echo "dns-dirs=$DNS_NAMES" >> "$GITHUB_OUTPUT"
gha_build_dns_snaps:
name: Build ${{ matrix.dns-dir }} ${{ matrix.build-for }}
+1 -1
View File
@@ -1,6 +1,6 @@
from setuptools import setup
version = '5.7.0.dev0'
version = '5.8.0.dev0'
setup(
version=version,
+1 -1
View File
@@ -1,6 +1,6 @@
from setuptools import setup
version = '5.7.0.dev0'
version = '5.8.0.dev0'
install_requires = [
# We specify the minimum certbot version as the current plugin
+1 -1
View File
@@ -1,6 +1,6 @@
from setuptools import setup
version = '5.7.0.dev0'
version = '5.8.0.dev0'
setup(
version=version,
+1 -1
View File
@@ -1,6 +1,6 @@
from setuptools import setup
version = '5.7.0.dev0'
version = '5.8.0.dev0'
setup(
version=version,
+1 -1
View File
@@ -2,7 +2,7 @@ import os
from setuptools import setup
version = '5.7.0.dev0'
version = '5.8.0.dev0'
install_requires = [
'cloudflare>=4.0',
+1 -1
View File
@@ -2,7 +2,7 @@ import os
from setuptools import setup
version = '5.7.0.dev0'
version = '5.8.0.dev0'
install_requires = [
'python-digitalocean>=1.15.0', # 1.15.0 or newer is recommended for TTL support
+1 -1
View File
@@ -2,7 +2,7 @@ import os
from setuptools import setup
version = '5.7.0.dev0'
version = '5.8.0.dev0'
install_requires = [
# This version of lexicon is required to address the problem described in
+1 -1
View File
@@ -2,7 +2,7 @@ import os
from setuptools import setup
version = '5.7.0.dev0'
version = '5.8.0.dev0'
install_requires = [
'dns-lexicon>=3.14.1',
+1 -1
View File
@@ -2,7 +2,7 @@ import os
from setuptools import setup
version = '5.7.0.dev0'
version = '5.8.0.dev0'
install_requires = [
'dns-lexicon>=3.14.1',
+1 -1
View File
@@ -2,7 +2,7 @@ import os
from setuptools import setup
version = '5.7.0.dev0'
version = '5.8.0.dev0'
install_requires = [
'google-api-python-client>=1.6.5',
+1 -1
View File
@@ -2,7 +2,7 @@ import os
from setuptools import setup
version = '5.7.0.dev0'
version = '5.8.0.dev0'
install_requires = [
'dns-lexicon>=3.14.1',
+1 -1
View File
@@ -2,7 +2,7 @@ import os
from setuptools import setup
version = '5.7.0.dev0'
version = '5.8.0.dev0'
install_requires = [
'dns-lexicon>=3.14.1',
+1 -1
View File
@@ -2,7 +2,7 @@ import os
from setuptools import setup
version = '5.7.0.dev0'
version = '5.8.0.dev0'
install_requires = [
'dns-lexicon>=3.14.1',
+1 -1
View File
@@ -2,7 +2,7 @@ import os
from setuptools import setup
version = '5.7.0.dev0'
version = '5.8.0.dev0'
install_requires = [
'dns-lexicon>=3.15.1',
+1 -1
View File
@@ -2,7 +2,7 @@ import os
from setuptools import setup
version = '5.7.0.dev0'
version = '5.8.0.dev0'
install_requires = [
# This version was chosen because it is the version packaged in RHEL 9 and Debian unstable. It
+1 -1
View File
@@ -2,7 +2,7 @@ import os
from setuptools import setup
version = '5.7.0.dev0'
version = '5.8.0.dev0'
install_requires = [
'boto3>=1.20.34',
+1 -1
View File
@@ -2,7 +2,7 @@ import os
from setuptools import setup
version = '5.7.0.dev0'
version = '5.8.0.dev0'
install_requires = [
'dns-lexicon>=3.14.1',
+1 -1
View File
@@ -1,6 +1,6 @@
from setuptools import setup
version = '5.7.0.dev0'
version = '5.8.0.dev0'
install_requires = [
# We specify the minimum certbot version as the current plugin
+7
View File
@@ -4,6 +4,13 @@ Certbot adheres to [Semantic Versioning](https://semver.org/).
<!-- towncrier release notes start -->
## 5.7.0 - 2026-07-15
### Fixed
- Fixed nginx configuration parsing when comments appear between tokens of a multi-line directive. ([#10598](https://github.com/certbot/certbot/issues/10598))
## 5.6.0 - 2026-05-11
### Changed
+1 -1
View File
@@ -147,7 +147,7 @@ options:
case, and to know when to deprecate support for past
Python versions and flags. If you wish to hide this
information from the Let's Encrypt server, set this to
"". (default: CertbotACMEClient/5.6.0 (certbot;
"". (default: CertbotACMEClient/5.7.0 (certbot;
OS_NAME OS_VERSION) Authenticator/XXX Installer/YYY
(SUBCOMMAND; flags: FLAGS) Py/major.minor.patchlevel).
The flags encoded in the user agent are: --duplicate,
+2
View File
@@ -336,6 +336,7 @@ pkcs12_ N Y Install certificates as PKCS#12 archives
dns-hetzner-cloud_ Y N DNS Authentication for Hetzner Cloud DNS
dns-czechia_ Y N DNS Authentication for czechia.com
dns-eurodns_ Y N DNS Authentication for EuroDNS
dns-dnscale_ Y N DNS Authenticator for DNScale
======================= ==== ==== =================================================================
.. _haproxy: https://github.com/greenhost/certbot-haproxy
@@ -373,6 +374,7 @@ dns-eurodns_ Y N DNS Authentication for EuroDNS
.. _dns-hetzner-cloud: https://github.com/rolschewsky/certbot-dns-hetzner-cloud
.. _dns-czechia: https://github.com/CZECHIA-COM/certbot-dns-czechia
.. _dns-eurodns: https://pypi.org/project/certbot-dns-eurodns/
.. _dns-dnscale: https://github.com/dnscaleou/certbot-dns-dnscale
If you're interested, you can also :ref:`write your own plugin <dev-plugin>`.
+1 -1
View File
@@ -1,4 +1,4 @@
"""Certbot client."""
# version number like 1.2.3a0, must have at least 2 parts, like 1.2
__version__ = '5.7.0.dev0'
__version__ = '5.8.0.dev0'
+1 -1
View File
@@ -1,6 +1,6 @@
from setuptools import setup
version = '5.7.0.dev0'
version = '5.8.0.dev0'
setup(
version=version,
-1
View File
@@ -1 +0,0 @@
Fixed nginx configuration parsing when comments appear between tokens of a multi-line directive.
+6
View File
@@ -103,6 +103,7 @@ tools/pip_install.py build towncrier uv virtualenv
root_without_le="$version.$$"
root="$RELEASE_DIR/le.$root_without_le"
REPO_ROOT="$(pwd)"
echo "Cloning into fresh copy at $root" # clean repo = no artifacts
git clone . $root
@@ -215,5 +216,10 @@ git commit -m "Remove built packages from git"
if [ "$RELEASE_BRANCH" = candidate-"$version" ] ; then
SetVersion "$nextversion".dev0
# If this message changes, it should also be changed in tools/finish_release.py
git commit -m "Bump version to $nextversion"
fi
cd $REPO_ROOT
git remote rm temp || :
git remote add temp $root && git fetch temp
+172 -6
View File
@@ -1,24 +1,38 @@
#!/usr/bin/env python
"""
Post-release script to publish artifacts created from GitHub Actions.
Post-release script to publish artifacts created from GitHub Actions, synchronize the repo
on Github, and print a formatted changelog for the latest release.
This currently includes:
* Moving snaps from the beta channel to the stable channel
* Pushing the candidate branch to GitHub
* Creating a minor release branch if it's not a point release
* Opening PR(s) to merge the release changes back into `main` and/or the minor release branch
* Printing a formmated changelog for the latest release for distribution
Setup:
- Install the snapcraft command line tool and log in to a privileged account.
- Install the `snapcraft` command line tool and log in to a privileged account.
- https://snapcraft.io/docs/installing-snapcraft
- Use the command `snapcraft login` to log in.
- Install the `gh` command line tool and log in to a privileged account.
- https://github.com/cli/cli#installation
- Use the command `gh auth login` to log in.
Run:
python tools/finish_release.py
python tools/finish_release.py [--skip-snaps] [--skip-github-sync] [--test version <VERSION_NUM>]
Testing:
This script can be safely run between releases. When this is done, the script
should execute successfully.
should execute successfully, if it has not run before. Promoting snaps from beta
and printing the changelog are idempotent, but synchronizing the github repo is not.
To skip syncing with github, use the `--skip-github-sync` flag.
To skip promoting snaps, use the `--skip-snaps` flag.
To test with a version other than the latest release, use the `--test-version <VERSION_NUM>` flag
with a version number formatted as <A.B.C>.
"""
@@ -45,6 +59,8 @@ ALL_SNAPS = ['certbot'] + PLUGIN_SNAPS
# This is the count of the architectures currently supported by our snaps used
# for sanity checking.
SNAP_ARCH_COUNT = 3
SKIP_SYNC_MESSAGE = ('To skip pushing updated branches to GitHub and creating PRs, '
'run this script with the `--skip-github-sync` flag.')
def parse_args(args):
@@ -61,6 +77,12 @@ def parse_args(args):
# Use the file's docstring for the help text and don't let argparse reformat it.
parser = argparse.ArgumentParser(description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter)
parser.add_argument('--test-version', type=str, default=None,
help='version in the form of 1.2.3, mainly for testing')
parser.add_argument('--skip-snaps', action='store_true',
help='don\'t promote snaps; used for testing')
parser.add_argument('--skip-github-sync', action='store_true',
help='don\'t synchronize branches to GitHub or create PRs')
return parser.parse_args(args)
@@ -171,6 +193,145 @@ def fetch_version_number():
assert len(version.split('.')) == 3
return version
def _run_silent_except_error(cmd: list[str], message: str = None) -> subprocess.CompletedProcess:
# For some reason, git prints a bunch of non-error output to stderr. Let's keep this script
# quiet by capturing it and only printing it if we hit an error.
try:
process = subprocess.run(cmd, check=True, universal_newlines=True, capture_output=True)
except subprocess.CalledProcessError as e:
print(f'Error running `{" ".join(cmd)}`')
if message is not None:
print(message)
print(e.output)
print(e.stderr)
print(SKIP_SYNC_MESSAGE)
raise e
else:
return process
def _create_pr(title: str, body: str, description: str, other_opts: list[str] | None = None) -> str:
cmd = ['gh', 'pr', 'create', '--title', title, '--body', body]
if other_opts is not None:
cmd = cmd + other_opts
try:
proc = subprocess.run(cmd, check=True, universal_newlines=True, capture_output=True)
except subprocess.CalledProcessError as e:
if 'already exists' in e.stderr:
print(f'{description} already exists...skipping creation. '
'To create a new PR, delete the old one on GitHub.')
# The error message looks like:
# a pull request for branch "candidate-4.24.0" into branch "main" already exists:
# https://github.com/certbot/certbot/pull/10698
last_e_word = e.stderr.split()[-1]
if 'https' in last_e_word:
output = last_e_word
else:
print(e.stderr)
print(SKIP_SYNC_MESSAGE)
raise e
else:
output = proc.stdout.rstrip()
return output
def _sync_candidate_from_temp_to_origin(version: str) -> None:
command_str = f'git pull temp candidate-{version}'
message = ('To run successfully, stash any changes you\'ve made to this branch. '
'Do not attempt to merge and continue, as that will fail.')
_run_silent_except_error(command_str.split(), message)
command_str = f'git push origin candidate-{version}'
message = ('To delete the branch on GitHub, run '
f'`git push origin --delete candidate-{version}`.')
_run_silent_except_error(command_str.split(), message)
def _create_release_pr_to_main(version: str) -> None:
print(f'Creating PR to merge candidate-{version} into main...')
title = f'update files from {version} release'
body = 'this PR only needs 1 review and should be merged, not squashed'
result = _create_pr(title, body, 'PR to merge release changes into main')
print(f'PR location: {result}')
def _create_release_pr_to_minor_branch(
version: str,
branch_name:str,
point_x_branch_name: str) -> None:
print(f'Creating PR to merge {branch_name} into {point_x_branch_name}...')
title = f'update files from {version} release'
body = 'this PR only needs 1 review and should be merged, not squashed'
pr_opts = [ '--head', branch_name,
'--base', point_x_branch_name]
result = _create_pr(title, body, 'PR to merge release changes into .x branch', pr_opts)
print(f'PR location: {result}')
def _create_and_push_branch_without_version_bump(version: str, branch_name: str) -> None:
# Usually a branch of form 1.2.x
# When it's a point release, it'll be any name, and then merged back into 1.2.x
print(f'Creating branch without version bump commit named {branch_name}...')
# Check if there are uncommited changes, since reset will blow them away
try:
msg = (f'Branch {branch_name} already exists. Delete it using '
f'`git branch -D {branch_name}`.')
_run_silent_except_error(f'git branch {branch_name}'.split(), msg)
_run_silent_except_error(f'git switch {branch_name}'.split())
# Make sure the last commit message is 'Bump version to {next version}'
output = _run_silent_except_error('git log -1 --pretty=%B'.split()).stdout
assert_msg = 'The most recent commit message should start with "Bump version to"'
assert output.startswith('Bump version to'), assert_msg
_run_silent_except_error('git reset --hard HEAD~1'.split())
msg = ('You shouldn\'t be trying to re-push a minor version branch. If you really want '
'to, go into GitHub, turn off branch deletion protection, and delete it there. ')
_run_silent_except_error(f'git push origin {branch_name}'.split(), msg)
finally:
# Switching to the current branch exits 0
_run_silent_except_error(f'git switch candidate-{version}'.split())
print('Created.')
def _check_branch_matches_version(version: str) -> None:
# This function assumes we're on a branch like `candidate-1.2.0` or `candidate-1.2.3`
# where the number after candidate should be equal to the version number
process = _run_silent_except_error('git branch --show'.split())
current_branch = process.stdout.rstrip()
if current_branch != f'candidate-{version}':
print(f'Unexpected branch name found. The current branch should be candidate-{version}.')
print(SKIP_SYNC_MESSAGE)
sys.exit(1)
def synchronize_github_repo(version: str):
_check_branch_matches_version(version)
message = ('You have uncommitted changes that will be deleted. '
'Stash your changes before rerunning this script.')
_run_silent_except_error('git diff --quiet HEAD'.split(), message)
_sync_candidate_from_temp_to_origin(version)
_create_release_pr_to_main(version)
# Check the last element of the version number to see if this is a point release
point_version = version.split('.')[-1]
point_release = point_version != '0'
point_x_branch_name = '.'.join(version.split('.')[:-1]) + '.x'
if not point_release:
branch_name = point_x_branch_name
else:
branch_name = f'point-candidate-{version}'
_create_and_push_branch_without_version_bump(version, branch_name)
if point_release:
_create_release_pr_to_minor_branch(version, branch_name, point_x_branch_name)
def generate_community_forum_post(version: str):
print('Generating announcement text for community forum post')
@@ -196,8 +357,13 @@ def generate_community_forum_post(version: str):
def main(args):
parsed_args = parse_args(args)
version = fetch_version_number()
promote_snaps(ALL_SNAPS, 'beta', version)
version = parsed_args.test_version
if not version:
version = fetch_version_number()
if not parsed_args.skip_snaps:
promote_snaps(ALL_SNAPS, 'beta', version)
if not parsed_args.skip_github_sync:
synchronize_github_repo(version)
generate_community_forum_post(version)
if __name__ == "__main__":
+1 -1
View File
@@ -90,7 +90,7 @@ matplotlib-inline==0.2.2 ; python_version >= "3.10" and python_version < "4.0"
mccabe==0.7.0 ; python_version >= "3.10" and python_version < "4.0"
mdurl==0.1.2 ; python_version >= "3.10" and python_version < "4.0"
more-itertools==11.1.0 ; python_version >= "3.10" and python_version < "4.0"
msgpack==1.1.2 ; python_version >= "3.10" and python_version < "4.0"
msgpack==1.2.1 ; python_version >= "3.10" and python_version < "4.0"
mypy-extensions==1.1.0 ; python_version >= "3.10" and python_version < "4.0"
mypy==1.9.0 ; python_version >= "3.10" and python_version < "4.0"
nh3==0.3.5 ; python_version >= "3.10" and python_version < "4.0"
+9 -6
View File
@@ -144,14 +144,17 @@ def _build_snap(
for arch in archs:
snap_path_list = glob.glob(join(workspace, f'{target}_*_{arch}.snap'))
assert len(snap_path_list) == 1
with open(snap_path_list[0], 'r') as f:
with open(snap_path_list[0], 'rb') as f:
first_block = f.read(10)
# 'hsqs' is the magic number that SquashFS files start with
if first_block.startswith(b'hsqs'):
continue
failed_archs.add(arch)
try:
first_line = f.readline().rstrip()
first_block_str = first_block.decode()
except UnicodeDecodeError:
first_line = ''
if first_line == "<!DOCTYPE html>":
failed_archs.add(arch)
print(f'The {target} {arch} snap file contains html instead of a snap')
first_block_str = first_block.hex(sep=',')
print(f'The {target} {arch} snap file began with invalid data: {first_block_str}')
dump_output = bool(failed_archs)
if not dump_output: