Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
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
34 changes: 34 additions & 0 deletions .github/workflows/actions-static-analysis.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
name: GitHub Actions Static Analysis

on:
push:
branches:
- "master"
paths:
- ".github/workflows/**"
pull_request:
paths:
- ".github/workflows/**"

permissions: {}

jobs:
zizmor:
name: Run zizmor
runs-on: ubuntu-latest
permissions:
contents: read
actions: read
steps:
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- name: Run zizmor 🌈
uses: zizmorcore/zizmor-action@3dc1ecc9bcb9e94e9b2c709687979e1298497054 # v0.6.2
with:
inputs: ".github/workflows/"
min-severity: medium
min-confidence: medium
advanced-security: false
14 changes: 11 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
name: CI
on: [push]

permissions:
contents: read

jobs:
python-tests:
runs-on: ubuntu-22.04
Expand All @@ -26,7 +30,9 @@ jobs:
- 6379:6379

steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- name: Apt update
run: sudo apt-get update -y
Expand Down Expand Up @@ -100,10 +106,12 @@ jobs:
javascript-tests:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- name: Setup NodeJS
uses: actions/setup-node@v2-beta
uses: actions/setup-node@27082cecf3ff7a1742dbd5e12605f0cb59dce2d9 # v2.1.3
with:
node-version: 24.14.0

Expand Down
7 changes: 5 additions & 2 deletions .github/workflows/new-issues.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,8 @@ on:

jobs:
add-to-hq:
uses: mitodl/ol-github-workflows/.github/workflows/add-to-ol-hq.yaml@main
secrets: inherit
permissions:
contents: read
uses: mitodl/ol-github-workflows/.github/workflows/add-to-ol-hq.yaml@edd566dec1edd920d35ab1fb60b589cddc44afad # main
secrets:
OL_HQ_PROJECT_SECRET: ${{ secrets.OL_HQ_PROJECT_SECRET }}
34 changes: 0 additions & 34 deletions .github/workflows/production.yml

This file was deleted.

34 changes: 0 additions & 34 deletions .github/workflows/release-candiate.yml

This file was deleted.

7 changes: 6 additions & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ repos:
- --exclude-files
- "_test.js$"
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: "v0.16.1"
rev: "v0.16.2"
hooks:
- id: ruff-format
- id: ruff
Expand Down Expand Up @@ -108,6 +108,11 @@ repos:
- id: actionlint
name: actionlint
description: Runs actionlint to lint GitHub Actions workflow files
- repo: https://github.com/zizmorcore/zizmor-pre-commit
rev: v1.29.0
hooks:
- id: zizmor
args: [--no-progress, --min-severity=medium, --min-confidence=medium]
- repo: local
hooks:
- id: drf-serializer-orm-check
Expand Down
11 changes: 1 addition & 10 deletions .secrets.baseline
Original file line number Diff line number Diff line change
Expand Up @@ -124,15 +124,6 @@
}
],
"results": {
".github/workflows/new-issues.yml": [
{
"type": "Secret Keyword",
"filename": ".github/workflows/new-issues.yml",
"hashed_secret": "3e26d6750975d678acb8fa35a0f69237881576b0",
"is_verified": false,
"line_number": 12
}
],
"authentication/utils.py": [
{
"type": "Secret Keyword",
Expand Down Expand Up @@ -245,5 +236,5 @@
}
]
},
"generated_at": "2026-04-13T13:24:33Z"
"generated_at": "2026-08-05T19:40:29Z"
}
19 changes: 19 additions & 0 deletions RELEASE.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,25 @@
Release Notes
=============

Version 0.198.0
---------------

- fix: revert pygsheets 2.0.6 (#4059)
- chore(deps): update dependency hls.js to v1.7.0 (#4048)
- fix(deps): update dependency django-anymail to v15.1 (#4056)
- fix(deps): update dependency pygsheets to v2.0.6 (#3757)
- fix(deps): update dependency boto3 to v1.43.63 (#4053)
- chore(deps): update dependency js-yaml to v5.3.0 (#4049)
- chore(deps): update dependency mocha to v11.8.0 (#4050)
- chore(deps): update dependency posthog-js to v1.417.0 (#4051)
- fix(deps): update dependency boto3 to v1.43.62 (#4046)
- [pre-commit.ci] pre-commit autoupdate (#4044)
- chore(deps): update actions/checkout action to v7.0.1 (#4045)
- Harden GitHub Actions supply-chain security with zizmor and delay dep updates (#4036)
- fix: make sync_db_to_hubspot resilient to bad data (#4019)
- fix(deps): update dependency mitol-django-authentication to v2026 (#4040)
- fix(deps): update dependency mitol-django-mail to v2026 (#4041)

Version 0.197.2 (Released August 18, 2026)
---------------

Expand Down
43 changes: 38 additions & 5 deletions hubspot_xpro/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from decimal import Decimal

from django.contrib.contenttypes.models import ContentType
from django.db import IntegrityError, transaction
from django.db.models import Q
from hubspot.crm.objects import SimplePublicObject, SimplePublicObjectInput
from mitol.hubspot_api.api import (
Expand Down Expand Up @@ -433,11 +434,43 @@ def get_hubspot_id_for_object(
raise_count_error=raise_error,
)
if hubspot_obj and hubspot_obj.id: # noqa: RET503
HubspotObject.objects.update_or_create(
object_id=obj.id,
content_type=content_type,
defaults={"hubspot_id": hubspot_obj.id},
)
try:
# Savepoint so we can catch IntegrityError and keep querying: on
# Postgres a failed statement aborts the whole transaction, and the
# atomic() block rolls back to the savepoint to keep the connection
# usable. See
# https://docs.djangoproject.com/en/stable/topics/db/transactions/#controlling-transactions-explicitly
with transaction.atomic():
HubspotObject.objects.update_or_create(
object_id=obj.id,
content_type=content_type,
defaults={"hubspot_id": hubspot_obj.id},
)
except IntegrityError:
mapping_conflict = (
HubspotObject.objects.filter(
content_type=content_type, hubspot_id=hubspot_obj.id
)
.exclude(object_id=obj.id)
.exists()
)
if not mapping_conflict:
# Not the expected duplicate-mapping conflict; surface the real
# DB integrity error rather than returning a hubspot id and
# continuing with a potentially inconsistent DB state.
raise
# The found hubspot id is already mapped to a different object of
# this content type (e.g. a duplicate-named product). Don't create a
# conflicting mapping, but still return the hubspot id so callers can
# proceed instead of failing the whole sync.
log.warning(
"Hubspot %s %s is already mapped to a different object; not "
"remapping %s %s",
content_type.model,
hubspot_obj.id,
content_type.model,
obj.id,
)
return hubspot_obj.id
elif raise_error:
raise ValueError(
Expand Down
53 changes: 53 additions & 0 deletions hubspot_xpro/api_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import pytest
from django.contrib.contenttypes.models import ContentType
from django.db import IntegrityError
from mitol.hubspot_api.factories import HubspotObjectFactory, SimplePublicObjectFactory
from mitol.hubspot_api.models import HubspotObject

Expand Down Expand Up @@ -357,6 +358,58 @@ def test_sync_product_hubspot_ids_dupe_names(mocker, mock_hubspot_api):
assert HubspotObject.objects.filter(content_type__model="product").count() == 2


def test_get_hubspot_id_for_object_skips_conflicting_mapping(mocker):
"""
When a lookup resolves to a hubspot id already mapped to a different object
(e.g. a duplicate-named product), get_hubspot_id_for_object should return the
hubspot id without raising IntegrityError or creating a conflicting mapping.
"""
existing_product = ProductFactory.create()
conflicting_product = ProductFactory.create()
content_type = ContentType.objects.get_for_model(Product)
HubspotObject.objects.create(
content_type=content_type,
object_id=existing_product.id,
hubspot_id="999",
)
mocker.patch(
"hubspot_xpro.api.find_product",
return_value=SimplePublicObjectFactory(id="999"),
)

result = api.get_hubspot_id_for_object(conflicting_product)

assert result == "999"
# No conflicting mapping was created for the second product
assert not HubspotObject.objects.filter(
content_type=content_type, object_id=conflicting_product.id
).exists()
# The original owner's mapping is intact
assert (
HubspotObject.objects.get(content_type=content_type, hubspot_id="999").object_id
== existing_product.id
)


def test_get_hubspot_id_for_object_reraises_unexpected_integrity_error(mocker):
"""
An IntegrityError that is not the expected duplicate-mapping conflict should
propagate rather than returning a hubspot id with an inconsistent DB state.
"""
product = ProductFactory.create()
mocker.patch(
"hubspot_xpro.api.find_product",
return_value=SimplePublicObjectFactory(id="555"),
)
mocker.patch(
"hubspot_xpro.api.HubspotObject.objects.update_or_create",
side_effect=IntegrityError("unexpected db problem"),
)

with pytest.raises(IntegrityError):
api.get_hubspot_id_for_object(product)


@pytest.mark.parametrize("match_all_lines", [True, False])
@pytest.mark.parametrize("match_all_deals", [True, False])
def test_sync_deal_hubspot_ids_to_hubspot(
Expand Down
Loading
Loading