Skip to content

fix(cycle): prevent editing completed cycles via sort_order payload#9438

Open
arhxam wants to merge 86 commits into
makeplane:masterfrom
arhxam:fix/9362-completed-cycle-immutable
Open

fix(cycle): prevent editing completed cycles via sort_order payload#9438
arhxam wants to merge 86 commits into
makeplane:masterfrom
arhxam:fix/9362-completed-cycle-immutable

Conversation

@arhxam

@arhxam arhxam commented Jul 19, 2026

Copy link
Copy Markdown

Description

The cycle update endpoints are meant to lock completed cycles (those whose end_date is in the past) so that only sort_order can be changed. In practice, any field — name, description, start_date/end_date — can be modified as long as sort_order is included alongside the other fields in the same PATCH.

Root cause: both the app view (plane/app/views/cycle/base.py) and the public API view (plane/api/views/cycle.py) correctly build a sanitized payload for completed cycles:

request_data = request.data
if cycle.end_date is not None and cycle.end_date < timezone.now():
    if "sort_order" in request_data:
        request_data = {"sort_order": request_data.get("sort_order", cycle.sort_order)}
    else:
        return Response({"error": "The Cycle has already been completed so it cannot be edited"}, ...)

…but then hand the original request.data (not the sanitized request_data) to the serializer:

serializer = CycleWriteSerializer(cycle, data=request.data, partial=True, ...)  # bug

So the sanitization is dead code and every field leaks through.

Fix: pass the sanitized request_data to the serializer in both views (and to the external_id conflict guard in the API view, so external_id/external_source are likewise ignored for completed cycles). Behavior for non-completed cycles is unchanged, since there request_data is request.data.

Type of Change

  • Bug fix (non-breaking change which fixes an issue)

Test Scenarios

Added two contract tests in plane/tests/contract/api/test_cycles.py:

  • test_update_completed_cycle_rejected — a PATCH with only name on a completed cycle returns 400 and leaves the cycle unchanged.
  • test_update_completed_cycle_only_changes_sort_order — a PATCH with sort_order plus name/description returns 200, applies only sort_order, and leaves name/description untouched (the regression this fixes).

py_compile passes on all touched files. The DB-backed contract suite requires PostgreSQL, which I could not stand up locally; the tests are written against the existing test_cycles.py fixtures and style.

References

Fixes #9362

dependabot Bot and others added 30 commits April 27, 2026 18:02
Bumps the npm_and_yarn group with 1 update in the /packages/tailwind-config directory: [postcss](https://github.com/postcss/postcss).


Updates `postcss` from 8.5.6 to 8.5.10
- [Release notes](https://github.com/postcss/postcss/releases)
- [Changelog](https://github.com/postcss/postcss/blob/main/CHANGELOG.md)
- [Commits](postcss/postcss@8.5.6...8.5.10)

---
updated-dependencies:
- dependency-name: postcss
  dependency-version: 8.5.10
  dependency-type: direct:production
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
* fix: filter out soft-deleted states from API endpoints

- Add deleted_at__isnull=True filter to StateListCreateAPIEndpoint.get_queryset()
- Add deleted_at__isnull=True filter to StateDetailAPIEndpoint.get_queryset()
- Prevents soft-deleted states from reappearing in UI after navigation
- Fixes makeplane#8829

* Fix: exclude issues linked to soft-deleted states
Drop four overrides that no package in the workspace depends on
(direct or transitive): js-yaml, happy-dom, tar-fs, and
@isaacs/brace-expansion. Verified against pnpm-lock.yaml — no resolved
entries existed, so the overrides were dead weight.
…eplane#9008)

`ProjectViewSet.partial_update`, `BulkEstimatePointEndpoint.partial_update`,
and `WorkspaceUserProfileEndpoint.get` previously fetched objects by primary
key alone after a workspace-scoped permission check, allowing an authenticated
caller to act on resources belonging to other workspaces by supplying a
foreign UUID with their own workspace slug in the URL.

- Project partial_update: scope `Project.objects.get` by `workspace__slug`,
  matching the existing pattern in `destroy`.
- Bulk estimate partial_update: scope `Estimate.objects.get` by
  `workspace__slug` and `project_id`, matching `retrieve` and `destroy`.
- Workspace user profile: require the target `user_id` to be an active
  member of the requested workspace before returning email and other PII.
…or (makeplane#8935)

X-Forward-For is not a real HTTP header — the standard is X-Forwarded-For.
With the typo, Nginx never replaces $remote_addr with the actual client IP,
so rate limiting and IP logging see the proxy IP instead of the real client.
Affects all three nginx configs (web, admin, space).
…tes (GHSA-x63v-p7wc-47x4) (makeplane#9014)

is_workspace_admin in ProjectMemberViewSet.partial_update was derived
from the target member's workspace role, not the requester's. When the
target happened to be a workspace admin, all three project-role guards
(L231/238/247) were bypassed regardless of who was making the request,
allowing a non-admin requester to re-role a workspace admin's project
membership. Compute is_workspace_admin from the requester instead and
keep the target's workspace role under a distinct name for the existing
new-role-vs-workspace-role cap.
…9044)

* chore: update completed_at logic updation in Issue save method

* fix: update error handling

Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>

* fix: use StateGroup

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
…eplane#8898)

* refactor(i18n): migrate packages/i18n from MobX to react-i18next with per-feature namespaces

Replaces the internals of packages/i18n with react-i18next while preserving the
identical public API. Consumer code using useTranslation() and TranslationProvider
requires no changes.

Translation file format: TS objects to JSON namespaces
- Converted TypeScript translation files (19 languages) into feature-based JSON namespace files
- Split the monolithic translations.ts into per-feature namespace files: workspace.json,
  project.json, work-item.json, cycle.json, inbox.json, etc.
- 30 community namespaces across 19 languages = 570 JSON files

Core runtime: MobX to i18next
- Replaced MobX TranslationStore with an i18next instance using i18next-icu
  (preserves ICU MessageFormat) and i18next-resources-to-backend (namespace lazy loading)
- useTranslation() and TranslationProvider keep identical signatures
- All namespaces pre-loaded during init for the current language to prevent
  re-render cascades
- Reads saved language from localStorage before init for faster first paint

Build tooling
- scripts/generate-types.ts: Reads English JSON files and outputs keys.generated.ts
  with a flat union of translation keys (runs before every build)
- scripts/sync-check.ts: Cross-locale missing/stale key detection, cross-namespace
  collision detection, path conflict detection (supports --ci mode)

App-level changes
- Removed useTranslation-based language sync effect from store-wrapper
- Language is now synced imperatively from profile.store (fetchUserProfile,
  updateUserProfile) and root.store (resetOnSignOut) via setLanguage()

Community scope
- Enterprise-only namespaces (customer, epic, initiative, pql, power-k, teamspace,
  release) excluded
- Enterprise-only keys pruned from shared namespaces (empty-state, navigation,
  project-settings, workspace-settings, work-item, importer, page, work-item-type)

* fix(i18n): restore parity with community preview after namespace refactor

The community port of plane-ee#6449 (MobX -> react-i18next refactor) had
gaps that broke ~25 unique translation keys community code calls. This
commit restores parity:

- Port power-k namespace (19 locales) from plane-ee, stripped of EE-only
  paths (initiative/customer/teamspace/dashboards/AI assistant). Community
  references 141 power-k keys that were entirely missing from the new
  per-locale JSON.
- Restore epic.* keys (8 leaves) into work-item.json across 19 locales —
  community ce/components/epics/* and quick-add issue forms reference
  them via isEpic conditional.
- Add 'date' leaf to common.json across 19 locales (sourced from
  work_item_types.settings.properties.property_type.date.label so the
  proper translation, not English, is used).
- Move exporter.* subtree from importer.json to common.json across 19
  locales — CSV export is a community feature, importer namespace is
  about to be deleted.
- Populate 7 empty Polish JSON files (common, empty-state, inbox, cycle,
  editor, automation, home) with EE Polish translations filtered to
  community key set. The community port committed these as 0-byte files.
- Drop EE-only namespaces with zero community usage: dashboard-widget,
  importer, intake-form (57 files across 19 locales).
- Update NAMESPACES const: drop the 3 deleted namespaces, add power-k.
- Fix 12 community call sites that referenced renamed/typo'd keys:
  account_settings.api_tokens.heading -> .title
  auth.common.password.toast.error.* -> .change_password.error.*
  sign_out.toast.error.* -> auth.sign_out.toast.error.*
  notification.toasts.un_snoozed -> .unsnoozed
  profile.stats.priority_distribution.priority -> common.priority
  projects.label -> common.projects
  progress -> common.progress
  epics -> common.epics
  creating_theme -> common.saving (no localized source available)
  toast.error (with trailing space typo) -> toast.error

Verified: every literal t(...) call in community apps/web, apps/admin,
apps/space, packages/* now resolves to a leaf key in the union of the
remaining 28 namespaces (English). The only remaining broken calls are
4 t('workspace') branch-key crashes — those are addressed by the next
commit (port of plane-ee#6763 crash guard).

Refs: makeplane/plane-ee#6449

* fix(i18n): guard t() against namespace-node returns to prevent React crashes

Wraps useTranslation()'s t() in coerceToString so namespace-node lookups
(which i18next-icu unconditionally returns as raw objects regardless of
returnObjects:false) fall back to the key string instead of crashing
React with 'Objects are not valid as a React child'.

Numbers and booleans are stringified; strings pass through; objects, null,
and undefined fall back to the key with a dev-mode console.warn pointing
to the bad call site. Production builds suppress the warning but keep the
guard. The wrapper can be removed once t() gains key-level type safety
(Phase 2 of the i18n roadmap).

Also pin returnObjects:false explicitly in the i18next config — it's the
default but documenting intent so it's not flipped by accident.

Audit-driven fix for 4 community call sites that hit this exact bug by
passing the branch key 'workspace' (which has nested children in the
workspace namespace) to t(). Switched to t('common.workspace') (existing
leaf with value 'Workspace').

Skipped EE-specific apps/web/core/components/initiatives/components/form.tsx
fix from upstream PR — initiatives is an enterprise feature not present
in community.

Refs: makeplane/plane-ee#6763

* chore(i18n): gitignore auto-generated translation key types

keys.generated.ts is a 4,000+ line union type regenerated deterministically
on every build (pnpm run generate:types) — should not be version-controlled.

Adding the file to .gitignore introduces a chicken-and-egg problem: turbo
runs check:types before build, but generate:types only ran as part of build.
On a fresh clone with no keys.generated.ts present, tsc --noEmit fails. Run
generate:types before tsc in check:types — same pattern as React Router apps
in this repo (react-router typegen && tsc --noEmit).

- Add packages/i18n/src/types/keys.generated.ts to root .gitignore
- Untrack the file from git (git rm --cached)
- Run generate:types before tsc in check:types

Verified: deleting keys.generated.ts and running check:types regenerates
the file correctly. After regeneration, git status shows the file remains
untracked (.gitignore is honored).

Refs: makeplane/plane-ee#6784

* fix(i18n): translate settings sidebar category headers

The 3 settings sidebar item-categories components were passing enum string
values directly to t() — e.g. t('your profile'), t('work-structure'),
t('administration'). These are not translation keys; they're enum identifiers,
so t() returned the raw key as fallback. Non-English users saw English text
in section headers (and English users only saw correct output thanks to CSS
text-capitalize masking the bug).

Added a CATEGORY_LABELS lookup map in each constants file that maps each
enum value to a real translation key. Components now call t(LABELS[category])
instead of t(category).

- Added 5 new keys to en/common.json common.* subtree:
  your_profile, developer, work_structure, execution, administration
  (English-only — non-English locales will fall back to English at runtime
  via i18next's fallbackLng, per the no-copy-paste-translations rule)
- Reused existing common.general and common.features for the categories
  whose labels already had translated keys
- Added PROFILE_SETTINGS_CATEGORY_LABELS, PROJECT_SETTINGS_CATEGORY_LABELS,
  WORKSPACE_SETTINGS_CATEGORY_LABELS in packages/constants/src/settings/
- Updated all 3 item-categories.tsx components

Found via comprehensive dynamic-key audit (1918 t() invocations classified
across literal, template-literal, property-access, conditional, function-call,
and identifier patterns). Same bug exists verbatim in plane-ee — fixing here
since the user requested no broken keys ship in community.

* chore: untrack Claude Code runtime lockfile

.claude/scheduled_tasks.lock is a session lockfile (sessionId, pid,
acquiredAt) created by Claude Code at runtime — accidentally tracked in
the i18n refactor commit. Untrack from git; the file stays on disk for
the running session.

* fix(i18n): type-safe coerceToString call + bump lint ceiling

Two post-Commit D follow-ups:

- Fix TS2379 in use-translation.ts: under exactOptionalPropertyTypes,
  i18next's t() overloads don't accept Record<string, unknown> | undefined
  as the second argument. Branch on whether params is defined and call
  the no-args or with-args overload accordingly.

- Bump @plane/i18n check:lint --max-warnings from 2 to 9. The package
  ships with 9 pre-existing warnings (8 prefer-toSorted in scripts/, 1
  no-named-as-default-member in instance.ts on a line untouched by my
  changes). plane-ee uses a workspace-level oxlint config without a
  per-package warning ceiling; matching the per-app pattern in this repo
  (web=11957, admin=759, space=676) is the smallest delta that keeps
  pnpm check:lint green.

Also includes formatter-pinned multi-line imports in 3 item-categories
files (oxfmt expanded them after Commit D added a third named import).

* fix(i18n): add packages/i18n/locales symlink to src/locales

The i18n refactor introduced resourcesToBackend with a dynamic import:
  import(`../locales/${language}/${namespace}.json`)

That path is relative to the source file's location. From src/core/instance.ts
it correctly resolves to src/locales/. But after tsdown bundling, the same
import call lives in dist/index.js, where ../locales/ resolves to
packages/i18n/locales/ — a directory that didn't exist. As a result the dev
server (which imports @plane/i18n via the package's exports field pointing
at dist/index.js) couldn't load any namespace, so every t() call returned
its key as fallback.

Add a symlink packages/i18n/locales -> src/locales so the dist-relative
path resolves correctly. Same fix plane-ee uses (verified: identical blob
mode 120000, SHA a4829b5). Keeps tsdown.config.ts and package.json on
the standard CE shape (exports: true, flat exports + main/module/types) —
EE's parallel conditional-exports setup is a separate refactor and out of
scope here.

* refactor(i18n): sync non-English locales to 100% parity with English

- All 18 non-English locales filled to 3,837/3,837 keys against the
  canonical English source. Stale keys removed, missing keys filled in
  with the appropriate per-locale translation.
- New scripts/lib/locale-io.ts module shared between sync-check and
  future tooling. readJsonFile() wraps JSON.parse errors with the
  offending file path so malformed locale JSON surfaces a useful
  filename in CI logs.
- New .github/workflows/i18n-sync-check.yml runs check:sync on PRs that
  touch packages/i18n/** and on push to preview. Fails any change that
  introduces missing or stale keys against English.
- Pin tsx@4.20.6 in the pnpm workspace catalog and declare it as a
  devDependency of @plane/i18n. Replace npx tsx@4.19.2 invocations with
  bare tsx so resolution goes through pnpm; npx currently resolves to a
  broken tsx@4.21.0 that pulls an unpublished esbuild range.

---------

Co-authored-by: Prateek Shourya <prateekshourya29@gmail.com>
…ectMember (makeplane#8966)

* test(api): add regression tests for create-project endpoint

Cover three scenarios:
- project_lead set to the creator's own user_id
- project_lead set to a different workspace member
- project_lead omitted (baseline)

The first two currently fail on preview because of a UUID coercion
bug in ProjectMember.objects.create — see follow-up commit.

* fix(api): pass project_lead_id (not User instance) when creating ProjectMember

The create-project endpoint built a ProjectMember row with
member_id=serializer.instance.project_lead, which resolves to a User
instance via Django's related descriptor instead of a UUID. Django's
UUIDField coercion then fails with AttributeError: 'User' object has
no attribute 'replace', which the generic exception handler converts
to a 400 "Please provide valid detail" — but only after the Project
row was already persisted, leaving an orphaned project without
default states.

Fix:
- Use project_lead_id (FK ID, no descriptor lookup) on both the guard
  comparison and the ProjectMember creation.
- Wrap the post-save flow in transaction.atomic() so any future
  exception triggers a clean rollback.
- Defer model_activity.delay() with transaction.on_commit() so the
  activity log only fires after a successful commit.
- Capture the exception with log_exception() in the generic catch so
  future regressions surface in api logs.

Note: a related data integrity issue exists where
ProjectCreateSerializer doesn't create a ProjectIdentifier row
(unlike its frontend counterpart). Out of scope here, will follow
up in a separate PR.

* fix(api): return 500 on unexpected errors and harden project create

Address review feedback from @sriramveeraghanta on PR makeplane#8966:

- The catch-all `except Exception` now returns 500 instead of 400.
  Reusing the generic 400 response on a server-side crash was the
  anti-pattern that hid the original ghost-create bug for nine months;
  a 500 lets clients distinguish between "bad input" and "server fault".
- The `IntegrityError` branch no longer falls through silently when the
  message is unrecognised. It re-raises so the catch-all `except` logs
  the exception and returns a 500.
- `transaction.on_commit()` now schedules `model_activity.delay` via
  `functools.partial` instead of a lambda, avoiding late-binding closure
  semantics.
- `ProjectCreateSerializer.validate()` now rejects `project_lead`
  values that are not active workspace members, surfacing the error
  under the `project_lead` field key (rather than as `non_field_errors`)
  so API clients can react programmatically.

* test(api): harden assertions and cover rollback / workspace-membership

Address review feedback from @sriramveeraghanta on PR makeplane#8966:

- The three existing tests now look up the created project via
  `Project.objects.get(id=response.data["id"])` instead of
  `.first()`. The assertion now fails for the right reason if the
  wrong project is returned by the endpoint.
- New `test_create_project_with_lead_not_in_workspace_returns_400`
  guards the workspace-membership validation added to
  `ProjectCreateSerializer.validate()`. Expects a 400 with a
  field-shaped error and zero rows persisted.
- New `test_model_activity_not_called_on_rollback` locks in the
  `transaction.on_commit()` semantics: when an exception is raised
  inside the atomic block (forced via mocking `State.objects.bulk_create`),
  the response is 500, no Project / ProjectMember / State rows are
  persisted, and the deferred `model_activity.delay` task is never
  dispatched. This prevents a future refactor from silently
  regressing the rollback contract.

* fix(api): mark on_commit dispatch as robust against broker failures

Address coderabbit re-review feedback on PR makeplane#8966.

Without robust=True, an exception raised by model_activity.delay
(e.g., a Celery broker outage) propagates out of the on_commit
callback and is caught by the outer `except Exception` handler,
which returns a 500 despite the project, ProjectMember rows and
default States having already been committed. The client sees a
500 and assumes the create failed — the same class of mismatch
between actual state and reported status that the original bug
exhibited, just at the post-commit phase.

Set robust=True so Django logs the dispatch failure internally
via the standard transaction logger and the response stays 201,
reflecting the persisted state.

Switch from `functools.partial` to a nested function
(`_dispatch_model_activity`) for the on_commit callable. Django's
robust on_commit logging path reads `func.__qualname__` to format
the error message; `partial` objects lack that dunder by default,
and the `functools.update_wrapper` workaround turns out to be
brittle when the wrapped callable is replaced by a Mock (which
the new regression test relies on). A nested function exposes
`__qualname__` natively, and the locals it closes over are
bound at definition time and never rebound before the callback
fires, so the late-binding-closure motivation for `partial` over
`lambda` does not apply here.

A new test, test_response_still_201_when_broker_dispatch_fails,
mirrors test_model_activity_not_called_on_rollback to lock in the
post-commit branch. It uses `@pytest.mark.django_db(transaction=True)`
so the surrounding test transaction is actually committed and the
`on_commit` callback fires (the default wrapper suppresses it via
rollback).

* fix(api): handle unrecognised IntegrityError consistently

Address coderabbit re-review feedback on PR makeplane#8966.

The previous fix used `raise` inside the IntegrityError handler with
the intent of "letting the catch-all `except Exception` below log it
and return 500". Coderabbit correctly flagged that `raise` exits the
try/except entirely — sibling except clauses don't fire — so
unrecognised integrity errors actually skipped `log_exception` and
the consistent 500 JSON shape, contradicting the stated intent.

Replicate the catch-all behaviour inline: log the exception via
`log_exception(e)` and return the same generic 500 response with
`{"error": "An unexpected error occurred"}`. The client now gets a
uniform error shape regardless of which `except` branch handled it.

---------

Co-authored-by: Jose Antonio Martinez <257598434+jamartineztelecoengineer84-dotcom@users.noreply.github.com>
makeplane#9024)

* fix: comment quick-actions menu hidden when no actions are available

* refactor: remove dead code
…ne#9099)

The community AIO Dockerfile declared the VOLUME instruction with
single quotes: VOLUME ['/app/data', '/app/logs']. Docker's JSON (exec)
form requires double quotes; with single quotes the line is parsed as
the shell form and the bracket/comma tokens become literal volume
paths ('[/app/data,' and '/app/logs]').

Docker tolerated these non-absolute anonymous volume paths at container
create time until Engine 29.5.0, which now rejects them with
"invalid mount config for type volume: invalid mount path: '[/app/data,'
mount path must be absolute", breaking `docker compose up --force-recreate`
and any container recreation for the AIO community image.

Switching to the valid JSON array form fixes the parsing.
…9094)

* fix(web): fallback when requestIdleCallback is unavailable

* refactor: improve idle task scheduling safety in render-if-visible
The webhook dispatcher validated webhook.url before posting but called
requests.post() without allow_redirects=False, so a webhook destination
could return a 3xx redirect to an internal address (cloud metadata,
internal services) and have the worker fetch it and persist the
response body to webhook_logs, readable back via the webhook-logs API.

Pass allow_redirects=False so the original validate_url() guard is
authoritative. Matches the pattern already used by safe_get() in
work_item_link_task.py and the behavior of GitHub/Stripe/Slack webhooks.
…ane#9138)

* chore(api): add docker compose test runner

Adds docker-compose-test.yml at the repo root that boots an isolated
postgres / valkey / rabbitmq / minio stack with health checks and tmpfs
data dirs, then runs pytest against it and exits. Includes a usage doc
under apps/api/tests/RUNNING_TESTS.md and a pointer in AGENTS.md.

Prereq: ./setup.sh (generates apps/api/.env).

Usage:
  docker compose -f docker-compose-test.yml up --build \
    --abort-on-container-exit --exit-code-from api-tests
  docker compose -f docker-compose-test.yml down -v

* fix(api): correct bugs surfaced by the pytest suite

Five small bugs caught by enabling the pytest contract suite end-to-end.
Each is independently justifiable:

- api/serializers/cycle.py + api/views/cycle.py: CycleCreateSerializer.validate
  required project_id in the request body, but the view only ever passes
  it through the URL kwarg. Cycle create/update via the public API was
  returning 400 "Project ID is required". Read project_id from
  serializer context (passed by the view) in addition to body/instance.

- app/views/api.py: ApiTokenEndpoint.get(pk) and patch(pk) did not filter
  out is_service=True tokens, so a user could read and modify service
  tokens through the user token endpoint. The list mode and delete
  already filter is_service=False; aligned the other two.

- bgtasks/work_item_link_task.py: validate_url_ip checked hostname before
  scheme, so file:///etc/passwd raised "No hostname found" instead of
  the documented "Only HTTP and HTTPS" error. Swapped the order so the
  scheme guard matches the docstring intent.

- utils/path_validator.py: get_allowed_hosts used `WEB_URL or APP_BASE_URL`
  so when both are configured to different hosts (the standard local
  setup: WEB_URL=:8000, APP_BASE_URL=:3000), only one was added to the
  allow-list. Redirects to APP_BASE_URL then had their next_path stripped
  because the host wasn't allowed. Include every configured base URL.

* chore(api): align pytest tests with current behavior, clear warnings

Test-side fixes paired with the product fixes in the previous commit, plus
deprecation cleanup that drops the test run from 104 warnings to 0.

Tests:
- tests/contract/api/test_cycles.py: project fixture sets cycle_view=True;
  the Project model defaults the flag to False, so cycle create/update
  always tripped "Cycles are not enabled for this project".
- tests/contract/app/test_authentication.py: next_path uses "/workspaces"
  (validate_next_path rejects values without a leading slash and returns
  empty, which dropped the path from the redirect URL).
- tests/unit/bg_tasks/test_copy_s3_objects.py: mocked sync_with_external_service
  now returns description_json; the task unconditionally writes the value
  back to the Issue, and Issue.description_json is NOT NULL on UPDATE.
- tests/unit/utils/test_url.py: three length-limit tests placed the URL at
  char 970+ on a single line, which contains_url truncates away as ReDoS
  defense (500-char per-line cap). Restructured to keep test intent intact
  while staying inside the per-line window.

Warning cleanup (104 → 0):
- settings/common.py: removed USE_L10N=True (deprecated in Django 4.0,
  removed in 5.0; default is True).
- celery.py, settings/local.py, settings/production.py: pythonjsonlogger
  moved jsonlogger → json; update the import / formatter path.
…ne#9146)

- Replace flat pr-description.md / release-notes.md with per-skill folders
- Add new branch-name and translate skills
- Update release-notes skill to match the GitHub Releases format (v1.2.0)
* fix: harden API token handling against rate-limit tampering and plaintext logging

- Make `allowed_rate_limit` read-only on APITokenSerializer so users can no
  longer raise their own API token rate limit via PATCH (GHSA-xfgr-2x3f-g2cf).
- Stop persisting API keys in plaintext in APITokenLogMiddleware: store a
  SHA-256 hash as the token identifier and redact sensitive request headers
  (X-Api-Key, Authorization, Cookie) before logging (GHSA-r5p8-cj3q-38cc).

* refactor: remove MongoDB log sink and add per-log-type retention

Logs are now written to and cleared from PostgreSQL only; MongoDB is no
longer used as a log sink or archive.

- Drop the MongoDB write/archival paths from the API request logger, the
  webhook log writer, and the cleanup tasks; Postgres is the sole sink.
- Cleanup tasks now hard-delete expired rows in batches via `all_objects`
  (rows are removed immediately, not soft-deleted).
- Add env-backed, per-log-type retention settings: API activity logs
  (API_ACTIVITY_LOG_RETENTION_DAYS, default 14), webhook logs
  (WEBHOOK_LOG_RETENTION_DAYS, default 14), email logs
  (EMAIL_LOG_RETENTION_DAYS, default 7). HARD_DELETE_AFTER_DAYS no longer
  drives any log cleanup.
- Delete settings/mongo.py, remove MONGO_DB_* settings and the plane.mongo
  loggers, and drop the pymongo dependency.

* chore: gitignore local advisories.md notes file

* fix: use keyed HMAC-SHA256 for API token log identifier

Address CodeQL "weak hashing of sensitive data" by hashing the API key with
a SECRET_KEY-keyed HMAC instead of a bare SHA-256. The identifier is a
non-reversible tokenization of a high-entropy key (not password storage);
keying it also prevents precomputing the digest from a known key value.

* chore: address review feedback on log cleanup and request logging

- process_logs accepts extra kwargs so jobs enqueued by an older release
  (with a mongo_log arg) don't fail during a rolling deploy.
- Log-cleanup batch delete failures are logged and skipped rather than
  aborting the run, so a single bad batch can't block the rest.
- Extend logger middleware test to assert Authorization and Cookie headers
  are redacted; add a test that a failing cleanup batch is swallowed.

* fix: fall back to default when a log retention env value is invalid

Negative (or unparseable) retention values would compute a future cutoff and
delete every log row. The retention settings now fall back to their defaults
in that case via a shared `_retention_days` helper.
…akeplane#9147)

* chore: bump turbo to 2.9.14, migrate pnpm config to workspace yaml

- Bump turbo from 2.9.4 to 2.9.14 in root package.json and the
  four production Dockerfiles (web, live, admin, space).
- Move pnpm.overrides, onlyBuiltDependencies, and
  ignoredBuiltDependencies from package.json into pnpm-workspace.yaml.
  pnpm v10+ no longer reads the pnpm field in package.json, so the
  full overrides block and most of onlyBuiltDependencies were being
  silently ignored.
- Add @plane/utils as a workspace dependency to the live server.

* chore: drop unused allowBuilds block, bump lodash-es to 4.18.1

- Remove the `allowBuilds` block from pnpm-workspace.yaml. It is not
  a recognized pnpm v10/v11 key and its values were inconsistent with
  the actual `onlyBuiltDependencies` / `ignoredBuiltDependencies`
  configuration.
- Bump `lodash-es` catalog entry from 4.18.0 to 4.18.1. With overrides
  now applied workspace-wide, 4.18.0 (marked deprecated as a "bad
  release") was being enforced everywhere.

* fix: use pnpm v11 allowBuilds in place of removed legacy keys

`onlyBuiltDependencies` and `ignoredBuiltDependencies` were removed
in pnpm v11. They were being silently ignored on this branch, which
caused `ERR_PNPM_IGNORED_BUILDS` to fail CI under `--frozen-lockfile`.

Replace them with the v11-native `allowBuilds:` block, mapping the
previous allowlist to `true` and the previous denylist (sharp) to
`false`. Locally verified that the build scripts for @parcel/watcher,
@swc/core, esbuild, and msgpackr-extract now run on install.
makeplane#9156)

* [WEB-7447] feat: migrate CE telemetry from OTLP traces to OTLP metrics

Replace span-based tracing (tracer.py) with OTLP observable gauges,
mirroring the approach already used in plane-ee. Key changes:

- Add otlp_endpoints.py — shared gRPC/HTTP endpoint helpers
- Add telemetry_metrics.py — push_instance_metrics task using
  MeterProvider + observable gauges (service name: plane-ce-api)
- User count excludes bots (is_bot=False)
- Page count excludes bot-owned private pages only
- Domain derived from WEB_URL env var
- Celery beat entry replaced with timedelta schedule +
  configurable METRICS_PUSH_INTERVAL_MINUTES (default 360 min)
- Add explicit opentelemetry-exporter-otlp-proto-grpc dep
- Delete tracer.py and telemetry.py (no longer needed)

Co-authored-by: Plane AI <noreply@plane.so>

* fix: address review comments on CE telemetry metrics

- harden grpc_endpoint_from_url for scheme-less OTLP_ENDPOINT values
  (e.g. "telemetry.plane.so:4317") by prepending "//" before urlparse
- fix WEB_URL domain extraction for scheme-less values with same approach
- replace N+1 workspace count queries (6×N) with 6 batched annotate(Count)
  aggregation queries — reduces DB load significantly at WORKSPACE_METRICS_LIMIT
- add deterministic ordering (order_by created_at) to workspace slice
- harden METRICS_PUSH_INTERVAL_MINUTES env parsing with try/except guard
  and positive-value validation to avoid crash on malformed input

Co-authored-by: Plane AI <noreply@plane.so>

* fix: cap METRICS_PUSH_INTERVAL_MINUTES to prevent timedelta overflow

Add upper-bound check (10_000_000 minutes) and catch OverflowError alongside
ValueError so an arbitrarily large env value cannot crash worker startup via
timedelta(minutes=...) OverflowError.

Co-authored-by: Plane AI <noreply@plane.so>

---------

Co-authored-by: Plane AI <noreply@plane.so>
* fix(web): add Safari fallback for requestIdleCallback

* fix(web): use globalThis in idle-task fallbacks

Switch idle-task fallback paths from window.* to globalThis.* so the
fallback no longer crashes in environments where window is undefined.
Also thread IdleRequestOptions through requestIdleFallback so the
caller's timeout hint is honored when falling back.

Addresses CodeRabbit review feedback on makeplane#9137.

---------

Co-authored-by: sriram veeraghanta <veeraghanta.sriram@gmail.com>
…token throttle (makeplane#9161)

- Define API_KEY_RATE_LIMIT in plane/settings/common.py and read it via
  django.conf.settings in ApiKeyRateThrottle instead of os.environ.
- Remove ServiceTokenRateThrottle and the service-token branch in
  BaseAPIView.get_throttles; all API key requests now go through
  ApiKeyRateThrottle.
makeplane#9163)

* fix(api): harden webhook & link-unfurl SSRF (advisory clusters A/B/C)

Resolves three overlapping SSRF advisory clusters around webhook delivery
and work-item link unfurling:

- Cluster A (private-IP validation + PATCH bypass): the webhook PATCH
  handler passed context={request: request} (the request object as the
  dict key) so the loopback/disallowed-domain guard silently no-op'd —
  now context={"request": request}. Hardened IP classification
  (is_blocked_ip) to also block multicast, unspecified, CGNAT
  (100.64.0.0/10), and IPv4 embedded in IPv6 transition addresses
  (IPv4-mapped, NAT64, 6to4, Teredo), robust across Python versions.

- Cluster B (DNS-rebinding TOCTOU): validators resolved DNS, then
  requests resolved it again at connect time. New pinned-IP client
  (plane/utils/url_security.py) resolves+validates once and connects to
  the validated IP literal so urllib3 performs no second lookup, while
  preserving Host header, TLS SNI and certificate verification against
  the real hostname.

- Cluster C (redirect SSRF): webhook delivery never follows redirects;
  the link crawler follows them manually, re-resolving + re-validating +
  re-pinning every hop.

Also: pin requests==2.33.0 in base.txt (imported directly; the pinning
adapter needs the >=2.32 get_connection_with_tls_context hook), and log
webhook URL-validation rejections to WebhookLog instead of swallowing
them.

Tests: new test_url_security.py (pinning, rebinding, redirect
re-validation, IP edge cases, TLS SNI) + updated link-task tests.
Full unit suite: 178 passed.

* fix(api): block OAuth avatar SSRF + add per-advisory SSRF regression tests

Verified every SSRF-class advisory against the current code. The webhook /
link / favicon reports — including the published CVE-2026-30242 and
CVE-2026-39843 and the newer "still bypassable" reports (DNS rebinding
GHSA-3856/-fgcv/-9292/-whh3/-4mjx/-6p39/-fv24/-8wvv, IP-classification gaps
GHSA-75fg, redirect GHSA-6v37/-jw6g/-mq87) — are resolved by the pinned-IP
client + hardened classifier in this branch.

The one SSRF family still unresolved was the OAuth avatar path:
download_and_upload_avatar() fetched the provider-supplied avatar_url with a
raw requests.get (no IP validation, default redirect following), so an
attacker-controlled avatar could reach internal addresses and be exfiltrated
via the static-asset endpoint (GHSA-cv9p-325g-wmv5, and the avatar hop of the
Gitea SSRF GHSA-hx79-5pj5-qh42). It now uses pinned_fetch_following_redirects,
which validates + pins every hop and blocks internal targets.

Adds test_ssrf_advisories.py: a per-advisory regression map covering webhook
IP validation, the PATCH context-key guard, webhook DNS rebinding, webhook
redirect, favicon redirect + rebinding, and OAuth avatar SSRF.

docker compose test: 199 unit tests pass.

* fix(api): address PR review feedback on the SSRF pinned client

- url_security: preserve URL-embedded credentials (user:pass@host) as Basic
  Auth instead of silently dropping them when rewriting to the IP literal
  (Copilot); bracket IPv6-literal hostnames in the Host header (Copilot);
  add stream=True support that keeps the session open until the response is
  closed, and release intermediate redirect hops.
- ip_address / work_item_link_task: treat UnicodeError (IDNA failures) from
  getaddrinfo as a resolution failure, not an uncaught exception (CodeRabbit).
- authentication/adapter/base: stream the avatar download so the size cap
  actually bounds memory, upload the size-bounded buffer (not response.content),
  and always close the response (CodeRabbit, major).
- tests: cover auth preservation, IPv6 Host bracketing, IDNA handling, and
  streamed session lifetime; drop an unused import.

docker compose test: 204 unit tests pass.
Centralize every external dependency version in the pnpm catalog
(pnpm-workspace.yaml) and reference them via `catalog:` across all
apps and packages. Packages that previously used differing versions
were unified to the highest (notably @react-pdf/renderer ^3.4.5 ->
^4.3.0 in apps/web).
…makeplane#9179)

* fix(api): return HTTP response from dispatch() exception handler

BaseAPIView.dispatch() and BaseViewSet.dispatch() built the proper
error Response via handle_exception() but returned the raw exception
object instead, causing Django to raise
"TypeError: 'Exception' object is not a valid HTTP response".

Fix all six occurrences across the api, app, license and space view
bases, and add a regression test covering every affected base class.

Fixes makeplane#9157

* chore(api): add copyright header to tests/unit/views/__init__.py

The empty package init file was missing the AGPL copyright header,
failing the Copy Right Check CI (addlicense -check on all tracked
.py files).
makeplane#9189)

The web/admin/space Docker image builds fail at the Vite/PostCSS step with
"Cannot find module '@tailwindcss/postcss'". These apps load the shared
@plane/tailwind-config/postcss.config.js, which references the @tailwindcss/postcss
plugin by name, but the plugin was only declared as a dependency of
packages/tailwind-config.

The Docker build installs via turbo prune + 'pnpm fetch' + 'pnpm install --offline',
which lays out node_modules so PostCSS resolves the plugin relative to the app
directory (apps/<app>), where it is not reachable. A plain 'pnpm install' resolves
it from tailwind-config's context instead, which is why local builds passed and
masked the issue.

Declare @tailwindcss/postcss as a direct devDependency of the three apps that run
Vite/PostCSS so it is symlinked into each app's node_modules and resolves under the
isolated linker regardless of install flow.

Verified by reproducing the exact Docker flow (prune -> fetch -> --frozen-lockfile
offline install -> build) for admin, space and web: all install in sync and build
successfully with full Tailwind CSS output.
* fix: bump npm deps to resolve Dependabot advisories

Resolve 8 open Dependabot alerts (all npm, in pnpm-lock.yaml) by bumping
the affected packages in pnpm-workspace.yaml and regenerating the lockfile:

- axios 1.15.2 -> 1.16.0 (catalog): CVE-2026-44494/44492/44490/44489
- tmp -> 0.2.6 (override): CVE-2026-44705 path traversal
- ws 8.x -> 8.20.1 (catalog + scoped override): CVE-2026-45736
- qs 6.14.2 -> 6.15.2 (override): CVE-2026-8723 DoS
- brace-expansion 5.0.5 -> 5.0.6 (override): CVE-2026-45149 DoS

brace-expansion and qs were pinned to their vulnerable versions in the
overrides block, so the pins had to be bumped directly. ws is scoped to
the 8.x major (ws@7.5.10 is below the vulnerable >=8.0.0 floor). All bumps
are semver-compatible patch/minor upgrades; no source changes required.

* fix: use named axios `create` import after 1.16.0 bump

axios 1.16.0 newly exposes `create` as a named export, so oxlint's
import/no-named-as-default-member rule now flags `axios.create(...)`.
That added one warning to @plane/services (7 > its --max-warnings=6
baseline) and to apps/web and apps/live, failing check:lint — surfaced
on this PR because the lockfile change busts Turbo's lint cache.

Switch the three `axios.create(...)` call sites to a named `{ create }`
import. `create` is a real value+type export in axios 1.16.0 (verified
via tsc). isCancel/CancelToken are left as `axios.*`: CancelToken is
only a type export (cannot be a value import under verbatimModuleSyntax)
and both were already counted within the existing baselines.

Verified locally: full `pnpm check:lint` (16/16) and `check:types`
(15/15) pass.
mguptahub and others added 24 commits July 1, 2026 17:34
… unauthenticated GET (makeplane#9305)

ProjectJoinEndpoint.get() was AllowAny and used ProjectMemberInviteSerializer
(fields = "__all__"), leaking the invitee's email and token to anyone who
knew the workspace slug, project ID, and invite UUID (GHSA-2r58-hgv7-635q).

Introduce ProjectMemberInvitePublicSerializer with an explicit safe field list
that excludes `email` and `token`, and swap it in for the public GET endpoint.
The full serializer is retained for authenticated admin viewsets.

Co-authored-by: Plane AI <noreply@plane.so>
…id, drop created_by overwrite (GHSA-5mxw-g5mw-3v3w) (makeplane#9315)

All three V2 issue attachment handlers (PATCH, DELETE, GET single) looked
up FileAsset by (pk, workspace, project_id) only — issue_id in the URL
was silently ignored. Any project member could target another user's
attachment UUID using their own issue_id, and PATCH would transfer
ownership via unconditional created_by = request.user.

Add issue_id=issue_id to all three FileAsset.objects.get() calls so the
lookup is correctly scoped to the attachment's owning issue. Remove the
created_by overwrite in PATCH — created_by is set at creation time and
must not be reassigned by a subsequent upload-confirm call.

Co-authored-by: Plane AI <noreply@plane.so>
…48-28jp-wr4p) (makeplane#9332)

* [WEB-7894] fix: eliminate TOCTOU race in InstanceAdminSignUp (GHSA-p548-28jp-wr4p)

Two concurrent POST requests to InstanceAdminSignUpEndpoint could both
pass the "no admin yet" check before either created the InstanceAdmin
row, resulting in dual instance admins.

Fix: wrap the check + create in transaction.atomic() with
select_for_update() on the Instance singleton row. The pre-check
(is_setup_done / existing admin) outside the lock is kept as a fast
early-exit for the common post-setup path. The re-check inside the
lock is the authoritative guard; user_login() is kept outside the
transaction to avoid holding the DB lock during session writes.

Co-authored-by: Plane AI <noreply@plane.so>

* fix: use global InstanceAdmin.objects.exists() guard (coderabbit)

The pre-check and re-check inside the atomic block were scoped to
filter(instance=instance), which could be bypassed if a stray second
Instance row existed. Changed both guards to InstanceAdmin.objects.exists()
to match the original global check and make them consistent with each other.

Co-authored-by: Plane AI <noreply@plane.so>

---------

Co-authored-by: Plane AI <noreply@plane.so>
…emails (makeplane#9307)

* fix: send notifications when work items are created or updated via the REST API

* test: rename contract test file to test_issue_notifications.py
Bot service accounts (User.is_bot=True, e.g. the WORKSPACE_SEED bot) are
internal identities meant to act only through API tokens. Nothing stopped
one from being driven through the interactive login flow if its email was
known, letting a human assume a service identity.

Reject bot accounts at the shared login chokepoint,
Adapter.complete_login_or_signup(), right beside the existing
deactivated-account check. This covers every interactive provider in one
place: email/password, magic code, and all OAuth providers (Google, GitHub,
GitLab, Gitea) across both the app and space surfaces. Bot API-token access
is left untouched, since that is how bots are meant to operate.

Also add a defense-in-depth is_bot guard to InstanceAdminSignInEndpoint,
which mints its own admin session outside the chokepoint (a bot is never an
InstanceAdmin today, so this is not currently reachable, but it closes the
path regardless).

Surface the rejection with a new dedicated error code
BOT_USER_LOGIN_FORBIDDEN (5017), plumbed into the app and space frontend
error helpers as well as the shared @plane/constants and @plane/utils
packages (message map + banner-alert list) so any consumer of the shared
auth-error handler renders it correctly. The admin path reuses the existing
ADMIN_AUTHENTICATION_FAILED code so it discloses no bot-specific error.

Add contract regression tests: a bot blocked via password and via magic
code, a bot blocked at the admin sign-in endpoint, and a non-bot control
that still logs in.
…prevent ATO (Cluster E) (makeplane#9289)

* [WEB-7778] fix(security): reject unverified OAuth provider emails to prevent ATO

An attacker controlling a self-hosted OAuth provider (Gitea, GitLab) could
assert any email address in the OAuth response and be matched to an existing
Plane account, bypassing authentication entirely.

- Add OAUTH_PROVIDER_UNVERIFIED_EMAIL (5124) error code
- GitHub: require both primary=True AND verified=True on email (was primary-only)
- Google: check verified_email=False field in userinfo response
- GitLab: check confirmed_at is non-null before accepting email
- Gitea __get_email: remove unverified fallbacks (primary-unverified, any-unverified)
- Gitea set_user_data: remove fast-path using .email from user object (no
  verification flag); always go through __get_email() which enforces verified

Fixes GHSA-7j95-vh8g-f365 (critical ATO).
Note: GHSA-cv9p-325g-wmv5 and GHSA-hx79-5pj5-qh42 (avatar SSRF) were
already fixed in PR makeplane#9163.

Co-authored-by: Plane AI <noreply@plane.so>

* fix(security): add read:user scope to Gitea; fail-closed on absent Google verified_email

Gitea's /api/v1/user/emails endpoint requires the read:user granular
scope — openid+email+profile alone is insufficient and __get_email()
would return a 401/403. Add read:user to the scope string.

Google: change default from True to fail-closed (is not True) so a
userinfo response that omits verified_email is rejected rather than
trusted. The service-account justification was incorrect — service
accounts do not go through the interactive OAuth2 callback flow.

Co-authored-by: Plane AI <noreply@plane.so>

---------

Co-authored-by: Plane AI <noreply@plane.so>
…invite accept/reject (makeplane#9308)

* fix(security): enforce token + auth validation on project invite accept/reject

ProjectJoinEndpoint.post() only checked that the caller-supplied email matched
the invited email — no token required, no authentication required.  Anyone who
knew the workspace slug, project ID, invite UUID, and invitee email could
accept or reject the invitation on the invitee's behalf (GHSA-g36h-p63v-g9c7).

Mirror WorkspaceJoinEndpoint.post() exactly:
- Validate `token` from request body against project_invite.token (→ 403 on mismatch)
- Require authenticated session (→ 401 if unauthenticated)
- Validate request.user.email against project_invite.email (→ 403 on mismatch)
- Remove the old request.data["email"] guard
- Use project_invite.email for downstream User lookup

Co-authored-by: Plane AI <noreply@plane.so>

* fix(security): address CR review on project invite token validation

- Use request.user directly instead of re-querying User by exact
  project_invite.email — avoids case-variant miss after the case-insensitive
  email check already validated the authenticated user (CR comment 1)
- Validate `accepted` as a real boolean before saving — form-encoded
  strings like "false" are truthy and could accidentally create memberships
  (CR comment 2)

Co-authored-by: Plane AI <noreply@plane.so>

---------

Co-authored-by: Plane AI <noreply@plane.so>
…tomLinkExtension (GHSA-v2vv-7wq3-8w2j) (makeplane#9313)

* [WEB-7888] fix(security): normalize href before protocol check in CustomLinkExtension (GHSA-v2vv-7wq3-8w2j)

The existing startsWith("javascript:") guard in parseHTML() and renderHTML()
is bypassable with a whitespace prefix (e.g. "\tjavascript:alert(1)"). Per the
WHATWG URL spec, browsers strip ASCII Tab/LF/CR from URL strings during parsing,
so the whitespace-prefixed href passes the guard, is rendered into the DOM
verbatim, and executes when clicked (browser strips the tab → javascript: fires).

Add isDangerousHref() helper that strips Tab/LF/CR and leading C0 controls
before the protocol check, replicating the browser's normalization. Replace
both naive startsWith checks in parseHTML() and renderHTML() with this helper.

Add a defence-in-depth guard in clickHandler.ts that rejects
javascript:/data:/vbscript: hrefs before window.open() — link.href is the
browser-resolved URL (whitespace already stripped), so a regex check there
catches any URI that bypasses the parse/render-time guards.

Co-authored-by: Plane AI <noreply@plane.so>

* [WEB-7888] fix: align clickHandler blocked-scheme list with isValidHttpUrl policy

Add file: and about: to the clickHandler protocol guard to match the
blocked-scheme contract in isValidHttpUrl, avoiding policy drift.

Co-authored-by: Plane AI <noreply@plane.so>

---------

Co-authored-by: Plane AI <noreply@plane.so>
…dated project IDs (GHSA-45hc-q4mw-jhxm) (makeplane#9333)

The `create` handler validated the network (SECRET/PUBLIC) check against
a workspace-scoped queryset but then used the raw client-supplied
`project_ids` list in the subsequent bulk_create and update calls.
An attacker could include UUIDs of projects from other workspaces: those
are absent from the validation queryset (no network check performed),
yet get inserted as ProjectMember rows via bulk_create(ignore_conflicts=True),
granting cross-workspace project access.

Fix: derive `validated_project_ids` from the filtered queryset (projects
already scoped to the requested workspace and passed the SECRET check),
and use it exclusively for all subsequent DB writes.

Co-authored-by: Plane AI <noreply@plane.so>
… endpoints (makeplane#9348)

Close a partial bypass of WEB-7813 (GHSA-2r95 / GHSA-w45q): the external
REST API project-list and work-item-list endpoints passed a raw order_by
query parameter to Django's .order_by(). Because Django resolves
__-separated relational paths, an attacker could order by sensitive
columns on related tables (created_by__password / token / email) to build
a blind ordering oracle, or crash the endpoint (HTTP 500) with an unknown
field.

Route both endpoints through the existing sanitize_order_by() helper with
the appropriate allowlist (PROJECT_ORDER_BY_ALLOWLIST, default sort_order;
ISSUE_ORDER_BY_ALLOWLIST, default -created_at), mirroring how
order_issue_queryset() already sanitizes. Non-allowlisted values collapse
to the safe default; legitimate orderings are unchanged.

Adds unit tests (allowlist neutralisation + passthrough) and contract
tests asserting both endpoints return 200 (not 500) for injected fields;
fail-before verified via git stash.

Advisory: GHSA-p885-6jpg-cr2p

Co-authored-by: Plane AI <noreply@plane.so>
…kspace/project (makeplane#9349)

* [WEB-8017] fix(security): sanitize order_by on external REST API list endpoints

Close a partial bypass of WEB-7813 (GHSA-2r95 / GHSA-w45q): the external
REST API project-list and work-item-list endpoints passed a raw order_by
query parameter to Django's .order_by(). Because Django resolves
__-separated relational paths, an attacker could order by sensitive
columns on related tables (created_by__password / token / email) to build
a blind ordering oracle, or crash the endpoint (HTTP 500) with an unknown
field.

Route both endpoints through the existing sanitize_order_by() helper with
the appropriate allowlist (PROJECT_ORDER_BY_ALLOWLIST, default sort_order;
ISSUE_ORDER_BY_ALLOWLIST, default -created_at), mirroring how
order_issue_queryset() already sanitizes. Non-allowlisted values collapse
to the safe default; legitimate orderings are unchanged.

Adds unit tests (allowlist neutralisation + passthrough) and contract
tests asserting both endpoints return 200 (not 500) for injected fields;
fail-before verified via git stash.

Advisory: GHSA-p885-6jpg-cr2p

Co-authored-by: Plane AI <noreply@plane.so>

* [WEB-8019] fix(security): scope CycleIssue reassignment lookup to workspace/project

CycleIssueViewSet.create looked up "issues already in another cycle" with
CycleIssue.objects.filter(~Q(cycle_id=cycle_id), issue_id__in=issues) —
without scoping to the caller's workspace/project. An ADMIN/MEMBER of their
own project could pass a work-item UUID from a different tenant and have that
foreign CycleIssue row reassigned to their cycle, silently evicting the
victim's work item from the victim's cycle (cross-tenant write / BOLA).

Scope the lookup to workspace__slug + project_id, mirroring the adjacent
create-path guard. Foreign-tenant rows are excluded from reassignment and
already dropped from the create path by the scoped new_issues query.

Adds a contract regression test proving a foreign-tenant CycleIssue row is
not reassigned (fail-before verified via git stash) plus a same-project
reassignment test to confirm the legitimate flow is unaffected.

Advisory: GHSA-4w5x-wc9w-f47x

Co-authored-by: Plane AI <noreply@plane.so>

---------

Co-authored-by: Plane AI <noreply@plane.so>
…ation (makeplane#9367)

* [WEB-8060] fix(security): enforce authz on is_active member (de)activation

ProjectMemberViewSet.partial_update nested every authorization guard inside
`if "role" in request.data:`. Because ProjectMemberSerializer exposes is_active
through fields="__all__" with no read-only, a project GUEST could PATCH
{"is_active": false} while omitting "role" to deactivate any member — including
admins — and take over the project (GHSA-hpgm-9r34-c4x5 / GHSA-25gg-cxm8-g7h9).

Add an independent is_active guard, mirroring the role block and destroy(): only
a project admin (or workspace admin) may (de)activate a member, and never one
whose role is equal to or higher than the requester's own.

Adds contract regression tests covering guest/member deactivation attempts and
the legitimate project-admin path.

Co-authored-by: Plane AI <noreply@plane.so>

* [WEB-8060] test(security): cover workspace-admin is_active bypass

Address Copilot review on makeplane#9367: add a positive-control test asserting a
workspace admin holding only a project GUEST role can still deactivate a project
admin (the intended is_workspace_admin bypass), so future changes cannot silently
remove it.

Co-authored-by: Plane AI <noreply@plane.so>

---------

Co-authored-by: Plane AI <noreply@plane.so>
…lane#9285)

Updated the `updateCurrentUser` method in `UserStore` to clone the current user data before making updates, ensuring that the original data remains unchanged during the update process. Additionally, added logic to update the local state with the new user data after a successful update.

fix(cover-image): return absolute URLs for cover images

Modified the `handleCoverImageChange` function to return absolute URLs for cover images, ensuring compatibility with the expected format. This change includes handling both uploaded images and new images, providing a consistent return structure.
…d inline (GHSA-ch8j-vr4r-qf6h) (makeplane#9312)

* [WEB-7887] fix(security): prevent stored XSS via SVG attachment served inline (GHSA-ch8j-vr4r-qf6h)

Add SCRIPT_CAPABLE_MIME_TYPES frozenset (image/svg+xml, text/javascript,
application/javascript, text/html, application/xhtml+xml, text/xml,
application/xml) and enforce Content-Disposition: attachment on three
download endpoints that previously defaulted to inline serving:

- GenericAssetEndpoint.get (api/views/asset.py)
- StaticFileAssetEndpoint.get (app/views/asset/v2.py)
- EntityAssetEndpoint.get (space/views/asset.py)

ATTACHMENT_MIME_TYPES is unchanged — users can still upload SVG, JS, and
XML files. The fix closes the XSS vector by ensuring script-capable assets
are always downloaded rather than rendered in the application's origin.

Co-authored-by: Plane AI <noreply@plane.so>

* [WEB-7887] fix: normalize MIME type before SCRIPT_CAPABLE_MIME_TYPES check

Strip MIME parameters and lowercase before the allowlist check so that
stored values like "image/svg+xml; charset=utf-8" or "Image/SVG+XML"
are correctly identified as script-capable and served as attachment.
Applies to all three download endpoints.

Co-authored-by: Plane AI <noreply@plane.so>

---------

Co-authored-by: Plane AI <noreply@plane.so>
…endpoints (makeplane#9347)

Add ISSUE_GROUP_BY_ALLOWLIST and validate group_by_field_name/
sub_group_by_field_name in BasePaginator.paginate() — the single chokepoint
all GroupedOffsetPaginator/SubGroupedOffsetPaginator callers funnel through
(the unauthenticated public deploy-board endpoint plus 5 GUEST-reachable
authenticated endpoints). Invalid fields now raise ParseError (HTTP 400)
instead of reaching F()/.values()/.order_by()/Window partition_by as a raw
ORM field name, which previously let an anonymous caller crash the endpoint
or force a blind relational-traversal oracle (GHSA-wwgj-929g-42cm).

Same field-name-injection class as the order_by fix (GHSA-2r95/GHSA-w45q,
WEB-7813), which never extended to group_by/sub_group_by.

Closes WEB-8012

Co-authored-by: Plane AI <noreply@plane.so>
…bership (makeplane#9372)

* [WEB-8066] fix: scope workspace asset get/patch/delete to project membership

WorkspaceFileAssetEndpoint is authorized at the WORKSPACE level, so any
workspace member/guest could reach get/patch/delete for a project-bound
asset (issue attachment/description, comment description, page description)
of a project they are not a member of — an incomplete fix of the GHSA-qw87
asset-IDOR cluster (GHSA-h7mc-p9mm-2r4w / GHSA-cjph-cgm5-8pw8).

Add project_membership_denied(): for project-bound assets (project_id set)
require an active ProjectMember of the asset's project, else 403. Workspace-
level entity types (WORKSPACE_LOGO, USER_AVATAR, USER_COVER) have project_id
NULL and remain accessible to any workspace member. Mirrors ProjectAssetEndpoint
(level=PROJECT). Guard runs before the is_uploaded check / mutation so a
non-member gets a uniform 403 and cannot probe upload state.

Contract regression tests cover denied get/patch/delete for a non-project
member, the positive project-member path, and the workspace-level exemption;
fail-before verified.

Co-authored-by: Plane AI <noreply@plane.so>

* [WEB-8066] harden: scope asset project-membership check to the asset's workspace

Address Copilot review: filter ProjectMember by workspace_id=asset.workspace_id
in addition to project_id, mirroring allow_permission's PROJECT branch. Prevents
a member of the same project in a different workspace from passing the check if
an asset row is ever inconsistent (asset.workspace_id != project.workspace_id).

Co-authored-by: Plane AI <noreply@plane.so>

* [WEB-8066] refactor: return bool from asset access helper, build Response in views

Address review (Saurabhkmr98): rename project_membership_denied ->
has_project_asset_access, returning a boolean (True = allowed) instead of a
Response. Each of get/patch/delete now builds the 403 Response based on the
returned value. Behaviour is unchanged (same 403 + message; workspace-level
assets with project_id=None still allowed).

Co-authored-by: Plane AI <noreply@plane.so>

---------

Co-authored-by: Plane AI <noreply@plane.so>
* chore: clean up React Doctor warnings in admin app

Raises the admin app's React Doctor score from 61 to 89 by resolving 49 of
53 diagnostics (3 errors + 46 warnings).

Errors (render purity):
- authentication/page.tsx: move ref write out of render into useEffect
- workspace/create/form.tsx: guard window.location.origin read
- sign-in-form.tsx: drop redundant setState-forwarding arrow

Accessibility:
- aria-labels on icon-only buttons (password toggles, sidebar, header)
- destination-naming aria-labels on ambiguous "learn more"/"here" links
- positive tabIndex -> 0; auth-banner dismiss div -> native <button>

Maintainability / bugs:
- delete 6 orphaned files; remove 3 unused deps (@tanstack/react-virtual,
  @tanstack/virtual-core, axios)
- hoist static form-field objects and pure helpers to module scope
- extract StoreContext into providers/store-context.ts (Fast Refresh)
- explicit button type; stable list key in sidebar-menu

Left in place: @react-router/node + isbot (required by react-router build,
false positives), String.includes in sidebar-menu (not array membership),
and the InstanceSetupForm split (cohesive form; deferred).

Note: committed with --no-verify; the pre-commit hook flags only pre-existing
unrelated lint warnings in the touched files. Changes pass check:types,
check:lint (759 cap), and check:format.

* chore: address PR review comments on admin react-doctor cleanup

- workspace/create/form.tsx: use useState with a lazy initializer + effect
  for workspaceBaseURL (removes the SSR-guard hydration concern and the
  per-render recompute)
- header: drop the always-true breadcrumb guard (keeps behavior; `> 0`
  would hide the root "Settings" crumb on top-level pages)
- remove tabIndex={-1} from password toggles and doc links so they are
  keyboard-accessible (setup-form, controller-input, gitea/github/gitlab/google)
- store-context: default StoreContext to undefined so the existing hook
  guards are live (fail-fast outside StoreProvider)
- store.provider: replace stale Next.js pages/ssg comment
- sidebar-menu: use startsWith for active-route detection (correct prefix
  match; also clears the js-set-map-lookups false positive)
…ore (makeplane#9245)

* refactor: migrate constants (fetch-keys) from apps/web to @plane/constants

* refactor: migrate constants (ai, calenda, gaant) from apps/web to @plane/constants

* refactor: migrate constants (sidebar, favorites) from apps/web to @plane/constants

* refactor: migrate constants (editor) from apps/web to @plane/constants

* refactor: migrate constants (plans) from apps/web to @core/components

* resolved lint errors

* fix: resolve coderabbit comments

* refactor: resolve coderabbit comments

* refactor: migrate hooks (use-file-size) from web/app/ce to web/app/core

* refactor: migrate hooks (use-notification-preview) from web/app/ce to web/app/core

* refactor: migrate hooks (use-timeline-chart) from web/app/ce to web/app/core

* refactor: migrate hooks (use-page, use-page-store) from web/app/ce to web/app/core

* refactor: migrate hooks (app-rail, indexes) from web/app/ce to web/app/core

* refactor: migrate hooks (use-page-flag) from web/app/ce to web/app/core

* refactor: migrate hooks (use-editor-flagging) from web/app/ce to web/app/core

* refactor: migrate hooks (use-filters-operator-configs) from web/app/ce to web/app/core

* refactor: migrate hooks (use-additional-editor-mention) from web/app/ce to web/app/core

* refactor: migrate hooks (use-additional-favorite-item-details) from web/app/ce to web/app/core

* refactor: migrate hooks (use-extended-editor-extensions, use-pages-pane-extensions) from web/app/ce to web/app/core

* refactor: migrate hooks (use-work-items-filters-config) from web/app/ce to web/app/core

* refactor: migrate hooks (use-extended-editor-config) from web/app/ce to web/app/core

* refactor: migrate hooks (use-bulk-operations) from web/app/ce to web/app/core

* refactor: migrate hooks (use-debounced-duplicate-issues) from web/app/ce to web/app/core

* refactor: migrate hooks (use-issue-properties) from web/app/ce to web/app/core

* refactor: migrate hooks (use-workspace-issue-properties) from web/app/ce to web/app/core

* refactor: delete hook (use-issue-embed) from web/app/ce

* fix: coderabbit comments

* fix: React doctor comments

* fix: import structure for hooks

* refactor: remove command palette & sidebar  components and related files from web/app/ce

* refactor: update analytics tab imports and add new analytics tab components

* feat: add project, work item, and workspace level modals for enhanced user interaction

* refactor: replace WorkspaceActiveCyclesRoot with WorkspaceActiveCyclesUpgrade and remove obsolete components

* refactor: migrate app-rail HOC to core components and remove obsolete index file

* refactor: remove unused automation components and simplify layout structure

* refactor: update import paths for CommonProjectBreadcrumbs and add new breadcrumb components

* refactor: update import path for WorkItemDetailRoot and add new work item detail component

* refactor: remove obsolete comments index file and introduce CommentBlock component in core

* refactor: update import paths for common components and introduce new ExtendedAppHeader, GlobalModals, and SubscriptionPill components

* refactor: remove obsolete index file and add MaintenanceMessage and InboxSourcePill components

* refactor: remove obsolete cycle components and introduce new cycle-related components in core

* refac: moved de-dupe directory to core

* refactor: add new desktop components and update import paths for sidebar functionality

* refactor: remove obsolete index file and introduce new version number and product updates components in core

* refactor: add EpicModal component and update import paths in issue layouts

* refactor: add HomePageHeader and HomePeekOverviewsRoot components, update import paths in home and issues sections

* refactor: remove obsolete home index file, update import paths for relation options in issue detail components, and introduce new activity helper functions

* refactor: remove AdditionalFilterValueInput from legacy path and reintroduce it in core filter value input component

* refactor: remove legacy workspace-notifications index file and introduce new notification components in core

* refactor: remove legacy license components and update import paths for PaidPlanUpgradeModal

* refactor: remove legacy navigation components and update import paths for navigation items in core

* refactor: introduce onboarding tour components and update import paths for tour-related files

* refactor: remove legacy theme switcher component and update import paths in profile settings

* refactor: update import paths for workflow components and introduce new workflow-related files in core

* refactor: remove legacy estimate components and introduce new estimate-related files in core

* refactor: remove legacy gantt-chart components and introduce new core components for gantt-chart functionality

* refactor: remove legacy helper components and introduce new access control and publish components in core

* refactor: introduce billing components and update import paths for billing-related files in core

* refactor: introduce new members components and update import paths for workspace members functionality

* refactor: update import paths for workspace components and introduce new workspace-related files

* refactor: remove deprecated components and clean up import paths across various modules

* refactor: remove unused components and clean up import paths across various modules

* refactor: remove unused sidebar components and update import paths in workspace notifications

* refactor: introduce new estimate, billing, and notification card components while updating import paths across various modules

* refactor: remove unused estimate and billing components, update import paths, and streamline workspace notification card structure

* refactor: update import paths for project components and remove unused files in the projects module

* refactor: remove unused power-k components and update import paths in the command palette module

* refactor: remove unused issue components and update import paths across the issues module

* refactor: remove unused mentions components and update import paths in the editor module

* refactor: remove unused components and update import paths across the pages module

* refactor: update type imports for issue properties in issue modal context

* fix: oxfmt

* fix: PR checks

* [GIT-254] Refactor: Store consolidation to @core/store (makeplane#9271)

* refactor: remove unused store files and update import paths across the application

* refactor: remove unused store files and update import paths across the core module

* refactor: remove unused issue filter and store files, and update import paths in the core module

* refactor: update import paths for timeline store files and introduce new base timeline store

* refactor: remove deprecated root store file and update import paths across the application

* refactor: update import paths for store files and correct root store type references

* refactor: standardize import comments and remove 'plane-web' artifacts across various components

* fix: CodeRabbit comments

* fix: format

* refactor: update TypeScript configuration and improve sorting method in TabNavigationRoot component

* refactor: replace toSorted with sort method for navigation item sorting in TabNavigationRoot component

---------

Co-authored-by: Rahulcheryala <rahulcheryala2004@gmail.com>
Co-authored-by: Prateek Shourya <prateekshourya29@gmail.com>
…bership (makeplane#9373)

* [WEB-8068] fix: scope workspace cycles/modules listing to project membership

WorkspaceCyclesEndpoint and WorkspaceModulesEndpoint are guarded only by
WorkspaceViewerPermission (any active workspace member) and filtered by
workspace__slug alone, letting any workspace member enumerate cycle/module
metadata (names, dates, issue counts) of private projects they are not a
member of (GHSA-wcc5-qgfr-8g9c).

Restrict both querysets to projects the requesting user is an active member
of, mirroring WorkspaceStatesEndpoint / WorkspaceLabelsEndpoint:
  project__project_projectmember__member=request.user
  project__project_projectmember__is_active=True
  project__archived_at__isnull=True
Add .distinct() to the Module query (the member join is to-many; Cycle already
had it).

Contract regression tests cover hidden cycles/modules for a non-project member,
the positive project-member path, and no row duplication; fail-before verified.

Co-authored-by: Plane AI <noreply@plane.so>

* [WEB-8068] refactor: drop unnecessary distinct() from module listing

Address Copilot review: the project-membership join is filtered to
request.user, and ProjectMember has a unique constraint on (project, member)
where deleted_at IS NULL, so the join yields at most one row per project and
cannot duplicate Module rows. distinct() was dead weight (and a planner cost
for large workspaces). Matches the reference WorkspaceStates/WorkspaceLabels
endpoints, which use no distinct().

Also drop the distinct-focused contract test: adding a *different* project
member never fans out the request.user-filtered join, so it would pass with or
without distinct() — misleading coverage.

Co-authored-by: Plane AI <noreply@plane.so>

---------

Co-authored-by: Plane AI <noreply@plane.so>
… membership (makeplane#9375)

The SAFE_METHODS branch of ProjectMemberPermission filtered ProjectMember by
workspace only (no project_id), so any workspace user who was a member of *some*
project could pass the check for a project they were not in. Consumers then
returned project-scoped data:
  - v1 ProjectMemberListCreateAPIEndpoint.get -> full project roster
    (GHSA-w2vf-m9x9-mvmc)
  - app DeployBoardViewSet.list -> project publish configuration (identical
    app-copy sibling)

Add project_id=view.project_id to the SAFE_METHODS filter in both copies
(utils + app), mirroring the non-safe branch and ProjectEntityPermission. A
non-member now receives 403.

Contract regression tests cover both endpoints: a workspace user who is a
member of a different project is denied (403) on a foreign project, while an
active member of the target project is allowed. Fail-before verified (both
denied cases leak 200 without the fix).

Co-authored-by: Plane AI <noreply@plane.so>
…r/ghcr) (makeplane#9380)

* [WEB-8095] fix: scope page-version reads to the URL project (GHSA-g49r/ghcr)

ProjectPagePermission verified the caller was a member of the URL
project_id but then resolved the page by workspace + page_id only, and
PageVersionEndpoint filtered versions the same way. A member of one
project could read the page versions of a public page belonging to a
different project in the same workspace via that project's URL
(GHSA-g49r-p85q-qq2w / GHSA-ghcr-frqr-6pqr).

- Scope the page lookup in ProjectPagePermission to projects__id via the
  ProjectPage M2M (both app/ and utils/ copies); deny when the page does
  not belong to the URL project.
- Scope PageVersionEndpoint list/detail querysets to
  page__projects__id=project_id (defense in depth); distinct() on the
  list guards against active + soft-deleted ProjectPage duplicates.
- Add contract regression tests (fail-before verified).

Co-authored-by: Plane AI <noreply@plane.so>

* [WEB-8095] fix: require active ProjectPage link when scoping pages to a project

Address CodeRabbit + Copilot review on makeplane#9380: projects__id=project_id
matched even soft-deleted ProjectPage links, so a page removed from the
project (link revoked) would still pass, and the version detail get()
could raise MultipleObjectsReturned on active + soft-deleted rows.

Put both conditions on the same project_pages relation in one filter so
they match a single ProjectPage row that is active:
project_pages__project_id=project_id + project_pages__deleted_at__isnull
=True. The partial-unique constraint (project, page WHERE deleted_at IS
NULL) then guarantees at most one row, so get() stays unambiguous and the
list needs no distinct(). Add a revoked-link regression test.

Co-authored-by: Plane AI <noreply@plane.so>

* [WEB-8095] fix: distinct() on page-version detail lookup as a MultipleObjectsReturned guard

Address CodeRabbit review on makeplane#9380. The active-link filter already keeps
the page__project_pages join to a single row via the partial-unique
constraint, but add distinct() to the detail get() as defense in depth so
the join can never surface MultipleObjectsReturned (a 500) even if that
invariant were ever violated.

Co-authored-by: Plane AI <noreply@plane.so>

---------

Co-authored-by: Plane AI <noreply@plane.so>
…e#9374)

* [WEB-8074] fix: scope IssueListEndpoint to guest created_by

IssueListEndpoint.get (/workspaces/<slug>/projects/<project_id>/issues/list/)
returned any issue whose id was passed in ?issues=, without the guest
created_by restriction its sibling IssueViewSet.list enforces. A project GUEST
(role=5) on a project with guest_view_all_features=False could read issues they
did not author by supplying their ids (GHSA-32c7-84jc-4w67).

Replicate the guest scope: when the requester is an active role=5 ProjectMember
and not project.guest_view_all_features, filter the queryset to
created_by=request.user. Applied to the base queryset so it flows through
filtering, annotation and grouping.

Contract regression tests cover the restricted guest (own-only), a full member
(sees all), and a guest with guest_view_all_features enabled (sees all);
fail-before verified.

Co-authored-by: Plane AI <noreply@plane.so>

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Plane AI <noreply@plane.so>
Co-authored-by: Dheeraj Kumar Ketireddy <dheeraj.ketireddy@plane.so>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
… modules (makeplane#9410)

* feat: add lite list endpoints for projects, members, cycles, and modules

* refactor: enhance order_by sanitization for cycle and module endpoints, update error handling for non-existent projects and workspaces
The cycle update endpoints (both the app and public API views) intend to
lock completed cycles so only sort_order can change. They build a sanitized
request_data dict containing just sort_order, but then hand the original,
full request.data to the serializer, so name/description/dates leak through
whenever sort_order is included in the same PATCH.

Pass the sanitized request_data to the serializer (and to the external_id
guard) so completed cycles are truly immutable except for sort_order.

Fixes makeplane#9362
@coderabbitai

coderabbitai Bot commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 61dd978a-3d95-4d73-9975-dcdf40487db2

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@CLAassistant

CLAassistant commented Jul 19, 2026

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you all sign our Contributor License Agreement before we can accept your contribution.
18 out of 19 committers have signed the CLA.

✅ sriramveeraghanta
✅ KanteshMurade
✅ MinitJain
✅ jamartineztelecoengineer84-dotcom
✅ sangeethailango
✅ b-saikrishnakanth
✅ bubacho
✅ astarte75
✅ pratapalakshmi
✅ mguptahub
✅ durgeshhhhhhh
✅ Rahulcheryala
✅ karthiksuki
✅ ch4og
✅ okxint
✅ codingwolf-at
✅ akhil-vamshi-konam
✅ wildsurfer
❌ arhxam
You have signed the CLA already but the status is still pending? Let us recheck it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.