Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@

## [Unreleased]

## [3.2.1] - 2026-06-17

- Fix QGIS plugin repository security scan issues blocking release:
add an explicit timeout to the OAuth callback unblock request, and
replace the high-entropy multipart form boundary with a readable
constant

## [3.2.0] - 2026-06-17

- Add support for QGIS 4.x (Qt6-based) releases, while remaining
Expand Down
35 changes: 24 additions & 11 deletions RELEASING.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,21 +11,27 @@ published, the [`release.yml`](.github/workflows/release.yml) workflow runs
`felt/metadata.txt` `version=` field — overriding whatever value is committed
there.

- Tags use **no `v` prefix** (e.g. `3.2.0`, not `v3.2.0`). The tag name becomes
- Tags use **no `v` prefix** (e.g. `3.2.1`, not `v3.2.1`). The tag name becomes
the version string verbatim, so a `v` prefix would ship a version literally
named `v3.2.0`.
- The `version=` field in `felt/metadata.txt` and the entries in
`CHANGELOG.md` are cosmetic — they do not drive the release and are not
prominently surfaced to users. Keep them consistent for hygiene, but the tag
is what matters.
named `v3.2.1`.
- The `version=` field in `felt/metadata.txt` is **cosmetic** — `qgis-plugin-ci`
overrides it with the tag name at release time. Keep it consistent with the
tag for hygiene, but the tag is what determines the shipped version.
- `CHANGELOG.md` is **not** cosmetic. At release time `qgis-plugin-ci` reads it
and injects the matching version's notes into the packaged `metadata.txt`
`changelog=` field, which QGIS surfaces to users in the Plugin Manager. So the
`changelog=` field in `metadata.txt` is intentionally left empty and must not
be hand-edited — maintain release notes in `CHANGELOG.md` only.

## Steps

1. **(Optional) Update the changelog and metadata on a branch.** Move items out
of `[Unreleased]` in `CHANGELOG.md` into a new `## [<version>] - <date>`
section, and bump `version=` in `felt/metadata.txt` to match. Open a PR and
merge to `main`. This is cosmetic hygiene, not required for the release to
succeed.
1. **Update the changelog and metadata on a branch.** Move items out of
`[Unreleased]` in `CHANGELOG.md` into a new `## [<version>] - <date>` section,
and bump `version=` in `felt/metadata.txt` to match. Open a PR and merge to
`main`. The release will still build without this, but the `CHANGELOG.md`
entry is what populates the user-visible changelog in the published package
(see Versioning above), so do it before tagging. The `version=` bump itself is
cosmetic hygiene since the tag overrides it.

2. **Create a GitHub Release** with a new tag (e.g. `3.2.0`), targeting `main`.
Publishing the release triggers [`release.yml`](.github/workflows/release.yml),
Expand All @@ -47,3 +53,10 @@ there.
builds an `-alpha` package via `qgis-plugin-ci package` and uploads it as a CI
artifact (with a download link posted on the PR). This is for testing
pre-release builds and is not part of the release path.
- **If plugins.qgis.org rejects the upload** (e.g. its automated security scan
blocks the package), do **not** reuse the same version number to resubmit.
The GitHub tag/release for that version is already cut against the old code
and should be treated as immutable. Fix the issues on a branch, bump to a new
patch version (e.g. `3.2.0` → `3.2.1`) with a matching `CHANGELOG.md` entry,
cut a new tag/release, and upload that. Leave the blocked release in place as
a record of the attempt.
15 changes: 11 additions & 4 deletions felt/core/api_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,9 @@ class FeltApiClient:
UPDATE_LAYER_ENDPOINT = '/maps/{}/layers'
LAYER_GROUPS_ENDPOINT = '/maps/{}/layer_groups'

# boundary marker used when building multipart/form-data upload bodies
MULTIPART_BOUNDARY = 'QGISFeltPluginFormBoundary'

def __init__(self):
# default headers to add to all requests
self.headers = {
Expand Down Expand Up @@ -338,23 +341,27 @@ def create_upload_file_request(self,
b'Host',
parameters.url[len('https://'):-1].encode()
)
boundary = self.MULTIPART_BOUNDARY
network_request.setRawHeader(
b"Content-Type",
b"multipart/form-data; boundary=QGISFormBoundary2XCkqVRLJ5XMxfw5")
"multipart/form-data; boundary={}".format(boundary).encode())

delimiter = "--{}\r\n".format(boundary).encode()
closing_delimiter = "--{}--\r\n".format(boundary).encode()

# build the form content as bytes: PyQt6 does not permit appending
# strings to QByteArray
form_content = b''
for name, value in parameters.to_form_fields().items():
form_content += b"--QGISFormBoundary2XCkqVRLJ5XMxfw5\r\n"
form_content += delimiter
form_content += b"Content-Disposition: form-data; "
form_content += f"name=\"{name}\"".encode()
form_content += b"\r\n"
form_content += b"\r\n"
form_content += str(value).encode()
form_content += b"\r\n"

form_content += b"--QGISFormBoundary2XCkqVRLJ5XMxfw5\r\n"
form_content += delimiter
form_content += b"Content-Disposition: "
form_content += \
f"form-data; name=\"file\"; filename=\"{filename}\"\r\n".encode()
Expand All @@ -364,7 +371,7 @@ def create_upload_file_request(self,
form_content += content

form_content += b"\r\n"
form_content += b"--QGISFormBoundary2XCkqVRLJ5XMxfw5--\r\n"
form_content += closing_delimiter

form_data = QByteArray(form_content)
content_length = form_data.length()
Expand Down
7 changes: 4 additions & 3 deletions felt/core/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -181,9 +181,10 @@ def force_stop():
"""
# we have to dummy a dummy request in order to abort the
# blocking handle_request() loop
# pylint: disable=missing-timeout
requests.get("http://127.0.0.1:{}".format(REDIRECT_PORT))
# pylint: enable=missing-timeout
requests.get(
"http://127.0.0.1:{}".format(REDIRECT_PORT),
timeout=10
)

def close_server(self):
"""
Expand Down
6 changes: 4 additions & 2 deletions felt/metadata.txt
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ name=Add to Felt
qgisMinimumVersion=3.22
qgisMaximumVersion=4.99
description=Create a collaborative Felt (felt.com) map from QGIS
version=3.2.0
version=3.2.1
author=Felt
email=support@felt.com

Expand All @@ -24,7 +24,9 @@ repository=https://github.com/felt/qgis-plugin

# Recommended items:

# Uncomment the following line and add your changelog:
# Leave this empty. The changelog is maintained in CHANGELOG.md and
# injected here automatically by qgis-plugin-ci at release time
# (see .github/workflows/release.yml). Do not edit this field by hand.
changelog=

# Tags are comma separated with spaces allowed
Expand Down
2 changes: 1 addition & 1 deletion felt/test/test_api_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -277,7 +277,7 @@ def test_create_upload_file_request(self):
b'form-data; name="file"; filename="test.gpkg"', body)
self.assertIn(b'GPKG\x00\x01binary', body)
self.assertTrue(
body.endswith(b'--QGISFormBoundary2XCkqVRLJ5XMxfw5--\r\n'))
body.endswith(b'--QGISFeltPluginFormBoundary--\r\n'))
self.assertEqual(request.rawHeader(b'Content-Length'),
str(len(body)).encode())

Expand Down
Loading