Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c14338cbce | |||
| 7a64730bd2 | |||
| 1803a09306 |
+11
-64
@@ -24,22 +24,17 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Cache pip wheels
|
# Cache step removed 2026-05-26: act_runner's cache backend has been
|
||||||
# continue-on-error: act_runner's cache backend has been broken on
|
# broken on this homelab runner since 2026-05-15 (first as request-
|
||||||
# this homelab setup since 2026-05-15 — bolt.db + cache/ dir exist
|
# timeout warnings, then as hard "Cannot find module .../dist/restore/
|
||||||
# but the action's JS bundle now fails to load at all
|
# index.js" failures that tank the whole job). The cache step targeted
|
||||||
# ("Cannot find module .../dist/restore/index.js"), hard-failing
|
# ~/.cache/pip but the install below uses `uv pip install` primarily,
|
||||||
# the whole job. The install step that follows handles cold caches
|
# whose own cache lives at ~/.cache/uv — so the cache step's real
|
||||||
# natively (uv pip / pip install both work without it), so net
|
# benefit was marginal even when working. Cost of removal: ~30s of
|
||||||
# cost of disabling the cache here is ~30s of wheel downloads per
|
# wheel downloads per job. Future re-enable: mount ~/.cache/uv as a
|
||||||
# job. Re-enable once the runner-side cache backend is fixed.
|
# docker volume at the runner level (skips actions/cache entirely),
|
||||||
continue-on-error: true
|
# or fix the runner-side cache backend (clear /var/run/act/actions/*,
|
||||||
uses: actions/cache@v4
|
# pin act_runner version, etc.).
|
||||||
with:
|
|
||||||
path: ~/.cache/pip
|
|
||||||
key: pip-${{ runner.os }}-py314-${{ hashFiles('requirements.txt') }}
|
|
||||||
restore-keys: |
|
|
||||||
pip-${{ runner.os }}-py314-
|
|
||||||
|
|
||||||
- name: Install Python deps
|
- name: Install Python deps
|
||||||
# ruff is pre-installed in the ci-python image (see CI-Runner/CI-python/
|
# ruff is pre-installed in the ci-python image (see CI-Runner/CI-python/
|
||||||
@@ -133,22 +128,6 @@ jobs:
|
|||||||
--health-retries 10
|
--health-retries 10
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- name: Cache pip wheels
|
|
||||||
# continue-on-error: act_runner's cache backend has been broken on
|
|
||||||
# this homelab setup since 2026-05-15 — bolt.db + cache/ dir exist
|
|
||||||
# but the action's JS bundle now fails to load at all
|
|
||||||
# ("Cannot find module .../dist/restore/index.js"), hard-failing
|
|
||||||
# the whole job. The install step that follows handles cold caches
|
|
||||||
# natively (uv pip / pip install both work without it), so net
|
|
||||||
# cost of disabling the cache here is ~30s of wheel downloads per
|
|
||||||
# job. Re-enable once the runner-side cache backend is fixed.
|
|
||||||
continue-on-error: true
|
|
||||||
uses: actions/cache@v4
|
|
||||||
with:
|
|
||||||
path: ~/.cache/pip
|
|
||||||
key: pip-${{ runner.os }}-py314-${{ hashFiles('requirements.txt') }}
|
|
||||||
restore-keys: |
|
|
||||||
pip-${{ runner.os }}-py314-
|
|
||||||
- name: API integration shard (resolve service IPs, migrate, test)
|
- name: API integration shard (resolve service IPs, migrate, test)
|
||||||
run: |
|
run: |
|
||||||
set -eux
|
set -eux
|
||||||
@@ -207,22 +186,6 @@ jobs:
|
|||||||
--health-retries 10
|
--health-retries 10
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- name: Cache pip wheels
|
|
||||||
# continue-on-error: act_runner's cache backend has been broken on
|
|
||||||
# this homelab setup since 2026-05-15 — bolt.db + cache/ dir exist
|
|
||||||
# but the action's JS bundle now fails to load at all
|
|
||||||
# ("Cannot find module .../dist/restore/index.js"), hard-failing
|
|
||||||
# the whole job. The install step that follows handles cold caches
|
|
||||||
# natively (uv pip / pip install both work without it), so net
|
|
||||||
# cost of disabling the cache here is ~30s of wheel downloads per
|
|
||||||
# job. Re-enable once the runner-side cache backend is fixed.
|
|
||||||
continue-on-error: true
|
|
||||||
uses: actions/cache@v4
|
|
||||||
with:
|
|
||||||
path: ~/.cache/pip
|
|
||||||
key: pip-${{ runner.os }}-py314-${{ hashFiles('requirements.txt') }}
|
|
||||||
restore-keys: |
|
|
||||||
pip-${{ runner.os }}-py314-
|
|
||||||
- name: Importer integration shard (resolve service IPs, migrate, test)
|
- name: Importer integration shard (resolve service IPs, migrate, test)
|
||||||
run: |
|
run: |
|
||||||
set -eux
|
set -eux
|
||||||
@@ -281,22 +244,6 @@ jobs:
|
|||||||
--health-retries 10
|
--health-retries 10
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- name: Cache pip wheels
|
|
||||||
# continue-on-error: act_runner's cache backend has been broken on
|
|
||||||
# this homelab setup since 2026-05-15 — bolt.db + cache/ dir exist
|
|
||||||
# but the action's JS bundle now fails to load at all
|
|
||||||
# ("Cannot find module .../dist/restore/index.js"), hard-failing
|
|
||||||
# the whole job. The install step that follows handles cold caches
|
|
||||||
# natively (uv pip / pip install both work without it), so net
|
|
||||||
# cost of disabling the cache here is ~30s of wheel downloads per
|
|
||||||
# job. Re-enable once the runner-side cache backend is fixed.
|
|
||||||
continue-on-error: true
|
|
||||||
uses: actions/cache@v4
|
|
||||||
with:
|
|
||||||
path: ~/.cache/pip
|
|
||||||
key: pip-${{ runner.os }}-py314-${{ hashFiles('requirements.txt') }}
|
|
||||||
restore-keys: |
|
|
||||||
pip-${{ runner.os }}-py314-
|
|
||||||
- name: Core integration shard (everything not api / importer / migration / phash / sidecar / scan / archive / backfill)
|
- name: Core integration shard (everything not api / importer / migration / phash / sidecar / scan / archive / backfill)
|
||||||
run: |
|
run: |
|
||||||
set -eux
|
set -eux
|
||||||
|
|||||||
@@ -83,55 +83,70 @@ def upgrade() -> None:
|
|||||||
if not other_ids:
|
if not other_ids:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# STEP 2: PRE-merge colliding Posts BEFORE the bulk reparent.
|
# STEP 2: PRE-merge ALL Posts with duplicate external_post_id
|
||||||
# Find pairs (keep, drop) where:
|
# across the entire (canonical + others) group, BEFORE the bulk
|
||||||
# keep = a Post under the canonical source
|
# reparent. Two cases must both be handled:
|
||||||
# drop = a Post under one of the non-canonical sources whose
|
# (A) canonical has Post X with epid=N; an "other" source has
|
||||||
# external_post_id equals keep.external_post_id
|
# Post Y with epid=N → after bulk UPDATE, (canonical, N)
|
||||||
# If we let the bulk UPDATE try to repoint drop onto canonical,
|
# collides with itself.
|
||||||
# uq_post_source_external_id fires row-by-row and aborts the
|
# (B) two different "other" sources each have a Post with
|
||||||
# whole migration.
|
# epid=N; canonical has none → after bulk UPDATE, both
|
||||||
colliding = conn.execute(
|
# are repointed to (canonical, N) and the second collides.
|
||||||
|
# The earlier version of this migration only handled (A); the
|
||||||
|
# operator's deploy 2026-05-26 tripped (B) at line 139.
|
||||||
|
# Fix: group ALL Posts in the (artist, platform) by epid; for
|
||||||
|
# any group with count>1, pick the keep (prefer one already
|
||||||
|
# under canonical; else lowest id) and merge the rest into it.
|
||||||
|
all_posts = conn.execute(
|
||||||
text("""
|
text("""
|
||||||
SELECT keep.id AS keep_id, drop_.id AS drop_id
|
SELECT external_post_id, id, source_id
|
||||||
FROM post AS keep
|
FROM post
|
||||||
JOIN post AS drop_
|
WHERE source_id = :canonical OR source_id = ANY(:others)
|
||||||
ON drop_.external_post_id = keep.external_post_id
|
ORDER BY external_post_id, id
|
||||||
AND drop_.id != keep.id
|
|
||||||
WHERE keep.source_id = :canonical
|
|
||||||
AND drop_.source_id = ANY(:others)
|
|
||||||
"""),
|
"""),
|
||||||
{"canonical": canonical_id, "others": other_ids},
|
{"canonical": canonical_id, "others": other_ids},
|
||||||
).fetchall()
|
).fetchall()
|
||||||
for keep_id, drop_id in colliding:
|
by_epid: dict = {}
|
||||||
conn.execute(
|
for epid, post_id, src_id in all_posts:
|
||||||
text("""
|
by_epid.setdefault(epid, []).append((post_id, src_id))
|
||||||
UPDATE image_provenance SET post_id = :keep
|
for epid, posts in by_epid.items():
|
||||||
WHERE post_id = :drop_
|
if len(posts) <= 1:
|
||||||
"""),
|
continue
|
||||||
{"keep": keep_id, "drop_": drop_id},
|
# Prefer a Post already under canonical as the keep.
|
||||||
)
|
canonical_posts = [p for p in posts if p[1] == canonical_id]
|
||||||
conn.execute(
|
if canonical_posts:
|
||||||
text("""
|
keep_id = canonical_posts[0][0]
|
||||||
UPDATE image_record SET primary_post_id = :keep
|
else:
|
||||||
WHERE primary_post_id = :drop_
|
keep_id = posts[0][0] # already sorted by id ASC
|
||||||
"""),
|
drop_ids = [p[0] for p in posts if p[0] != keep_id]
|
||||||
{"keep": keep_id, "drop_": drop_id},
|
for drop_id in drop_ids:
|
||||||
)
|
conn.execute(
|
||||||
# Repointed provenance may now collide on
|
text("""
|
||||||
# uq_image_provenance_image_post (alembic 0021). Dedupe:
|
UPDATE image_provenance SET post_id = :keep
|
||||||
# keep min(id) per (image_record_id, post_id).
|
WHERE post_id = :drop_
|
||||||
conn.execute(text("""
|
"""),
|
||||||
DELETE FROM image_provenance ip1
|
{"keep": keep_id, "drop_": drop_id},
|
||||||
USING image_provenance ip2
|
)
|
||||||
WHERE ip1.image_record_id = ip2.image_record_id
|
conn.execute(
|
||||||
AND ip1.post_id = ip2.post_id
|
text("""
|
||||||
AND ip1.id > ip2.id
|
UPDATE image_record SET primary_post_id = :keep
|
||||||
"""))
|
WHERE primary_post_id = :drop_
|
||||||
conn.execute(
|
"""),
|
||||||
text("DELETE FROM post WHERE id = :drop_"),
|
{"keep": keep_id, "drop_": drop_id},
|
||||||
{"drop_": drop_id},
|
)
|
||||||
)
|
# Repointed provenance may now collide on
|
||||||
|
# uq_image_provenance_image_post (alembic 0021). Dedupe.
|
||||||
|
conn.execute(text("""
|
||||||
|
DELETE FROM image_provenance ip1
|
||||||
|
USING image_provenance ip2
|
||||||
|
WHERE ip1.image_record_id = ip2.image_record_id
|
||||||
|
AND ip1.post_id = ip2.post_id
|
||||||
|
AND ip1.id > ip2.id
|
||||||
|
"""))
|
||||||
|
conn.execute(
|
||||||
|
text("DELETE FROM post WHERE id = :drop_"),
|
||||||
|
{"drop_": drop_id},
|
||||||
|
)
|
||||||
|
|
||||||
# STEP 3: Bulk reparent the remaining Posts off the other
|
# STEP 3: Bulk reparent the remaining Posts off the other
|
||||||
# Sources. After step 2, no collisions on
|
# Sources. After step 2, no collisions on
|
||||||
|
|||||||
Reference in New Issue
Block a user