Skip to content

chore: sync with upstream Postiz (2026-09-21) - #43

Merged
JOY (JOY) merged 11 commits into
devfrom
upstream-sync-20260921
Sep 21, 2026
Merged

JOY (JOY) merged 11 commits into
devfrom
upstream-sync-20260921

Conversation

@JOY

Copy link
Copy Markdown

What kind of change does this PR introduce?

CI/CD / Upstream sync. Manually completed version of the daily automated sync: merges upstream gitroomhq/postiz-app main (6 commits, head 7cef69c1) into dev. Brings in the YouTube clipping workflow (REST + MCP tools + widget), MCP upload widget, Mastra 1.67 schema tables, staging-conflicts workflow fix, and the GAdvisory scope security change. Three conflicts resolved by hand: .env.example (kept the fork Temporal section and accepted the upstream Cloudflare/RUNPOD/Deepgram block), libraries/nestjs-libraries/src/chat/start.mcp.ts (kept the fork crove_post_* hidden-tool aliases and added the three new clipping tools plus their branded aliases to the Claude-hidden list), libraries/nestjs-libraries/src/chat/load.tools.service.ts (kept the fork dynamic agentId + brand-name description, which defaults to postiz and is a superset of upstream).

Why was this change needed?

The automated sync workflow aborted because the merge was not clean; the corridor must stay current so upstream fixes (provider fixes, clipping feature, security hardening) keep flowing in small, reviewable increments instead of piling up (per docs/adr/0001-upstream-sync-and-fork-delta.md).

Technical Details & Scope

  • Merge commit 03e4f51 on branch upstream-sync-20260921, base dev.
  • Conflict resolutions limited to the three files above; every other file took the automatic merge result (schema.prisma auto-merged including upstream Mastra table additions).
  • No fork features changed; no schema migration authored in this repo (upstream Prisma schema changes flow through the merge and are applied via the existing prisma db push pipeline).

Verification & Testing

  • git grep confirms zero conflict markers remain in the worktree.
  • Resolved TS files kept fork branding behavior (branded MCP server name, crove_post_* alias coverage) while accepting upstream additions.
  • CI on this PR runs the full gates: build.yml (Postgres/Redis services + bootstrap suites + build) and branding-guard.yml.

QA

  1. Open the PR Files changed - confirm only the three resolved files show hand edits (env.example, start.mcp.ts, load.tools.service.ts) and the rest are upstream content
  2. In start.mcp.ts confirm the claudeHiddenTools list contains both the fork crove_post_* aliases and the new clipping tools
  3. In load.tools.service.ts confirm the Agent id/name still use the agentId parameter with the brand-name description
  4. In .env.example confirm the Temporal section and the upstream RUNPOD/Deepgram block both survive with no conflict markers
  5. CI: build.yml green (bootstrap suites 53 passing) and branding-guard.yml green on this PR

Checklist:

  • My code follows the project's code style and architectural conventions.
  • Local build passes (pnpm run build) - not run locally in this worktree; CI build.yml on this PR is the confirmation gate.
  • Branding guard validation passes (pnpm dlx tsx scripts/branding-guard.ts) - CI branding-guard.yml on this PR is the confirmation gate.
  • Tests and typecheck have been verified without errors - CI bootstrap suites on this PR are the confirmation gate.
  • Documentation has been updated (if applicable) - sync needs no doc change; corridor policy already recorded in ADR-0001 (docs: minimal batch groundwork - ADR-0001, fork-delta inventory, CLAUDE.md corrections #42).
  • No secrets or sensitive credentials are included in this PR.
  • I have filled in the QA / Verification section above with real steps to verify this change.

…and widget

Turns a YouTube video into captioned vertical clips that land in the media
library and as draft posts. Adds the Clipping / ClippingClip models, the
clipping_minutes credit type, clippingWorkflow + per-clip child workflows,
the /clipping REST routes, the MCP clipping tools and the ui://postiz/clipping
status widget.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
- createClips takes the row of the clipping before looking for clips, so a
  timed out attempt and its retry can no longer both store a set
- a clip is claimed (draftedAt off null) before its draft is created and
  released when creation fails, so a retry cannot draft it twice
- urls are stripped from the logged processor failure
- Turkish billing label reads correctly after the number

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…time

createPost writes one post per channel, so a failure on a later channel left
the earlier drafts in place while the released claim let a retry create them
again. The claim now stays, the free slot is looked up before it is taken, and
each channel is drafted on its own so one failing channel does not drop the rest.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
feat(clipping): YouTube video clipping workflow (REST + MCP tools + widget)
# Conflicts:
#	.env.example
#	libraries/nestjs-libraries/src/chat/load.tools.service.ts
#	libraries/nestjs-libraries/src/chat/start.mcp.ts

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces an AI-powered video clipping feature that allows users to turn long YouTube videos into short vertical clips with burned-in captions. It adds backend controllers, middleware, and database schemas, alongside Temporal workflows and activities to manage the multi-step clipping process (analysis, transcription, clipping, rendering, and drafting). Additionally, it integrates subscription-based clipping limits, UI billing updates, localized translations, and Mastra agent tools with an MCP widget. The review feedback highlights three key issues: the use of an invalid OpenAI model name (gpt-4.1) which will fail at runtime, an unsafe non-null assertion on a potentially null organization object, and a potential workflow crash from unhandled JSON parsing of clipping integrations.

const { clips } = (
await openai.chat.completions.parse(
{
model: 'gpt-4.1',

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

gpt-4.1 is not a valid OpenAI model name. This will cause the API call to fail with a 400 Bad Request error at runtime. Please use a valid model name such as gpt-4o or gpt-4o-mini which support structured outputs (response_format with Zod schemas).

Suggested change
model: 'gpt-4.1',
model: 'gpt-4o',

Comment on lines +165 to +169
const org = await this._organizationService.getOrgByIdWithSubscription(
organizationId
);
return (await this._subscriptionService.checkCredits(org!, CREDITS_TYPE))
.credits;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Using the non-null assertion operator (org!) here is unsafe because getOrgByIdWithSubscription can return null if the organization is not found or has been deleted. This will cause a runtime TypeError. Please handle the null case safely.

Suggested change
const org = await this._organizationService.getOrgByIdWithSubscription(
organizationId
);
return (await this._subscriptionService.checkCredits(org!, CREDITS_TYPE))
.credits;
const org = await this._organizationService.getOrgByIdWithSubscription(
organizationId
);
if (!org) {
return 0;
}
return (await this._subscriptionService.checkCredits(org, CREDITS_TYPE))
.credits;

Comment on lines +922 to +931
const integrations = (
await Promise.all(
(JSON.parse(clipping.integrations) as string[]).map((id) =>
this._integrationService.getIntegrationById(
clipping.organizationId,
id
)
)
)
).filter((f) => f && !f.deletedAt && !f.disabled);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Parsing clipping.integrations directly with JSON.parse without a try-catch block can cause the background Temporal workflow to crash if the database contains invalid JSON or a non-array value. Wrapping it in a try-catch block makes the workflow more resilient.

    let integrationIds: string[] = [];
    try {
      integrationIds = JSON.parse(clipping.integrations) as string[];
    } catch (err) {
      console.error('Failed to parse clipping integrations:', err);
    }

    const integrations = (
      await Promise.all(
        integrationIds.map((id) =>
          this._integrationService.getIntegrationById(
            clipping.organizationId,
            id
          )
        )
      )
    ).filter((f) => f && !f.deletedAt && !f.disabled);

…ools

The alias map in load.tools.service.ts does not register crove_post_ aliases
for the clipping tools, so those claudeHiddenTools entries could never match.
The real tool names stay hidden; re-add branded aliases only if the alias map
gains clipping entries.
Upstream's clipping workflow added OpenaiService.pickClips referencing a
module-level openai client that the fork replaced with getOpenAIClient() /
getModel() for OPENAI_BASE_URL and OPENAI_MODEL_NAME support, so the
auto-merge produced TS2552 (Cannot find name 'openai'). Align pickClips with
the fork pattern used by every other method in this service.
…age code

- clipping.service.ts: log clipping failures with fixed format strings and
  data as arguments instead of interpolating tainted values into the template
- local.storage.ts: contain removeFile to the upload directory - resolve the
  requested path and refuse to unlink anything outside the upload root, so a
  traversal-shaped key can never delete arbitrary files

Both fixes touch files introduced/changed by this upstream sync; they are
deliberate fork deltas to be recorded in docs/fork-delta.md.
CodeQL js/path-injection does not model the startsWith(resolvedRoot + sep)
prefix check as a validated boundary. path.relative + isAbsolute + '..'
rejection is the canonical containment form: any path resolving outside the
upload root yields a relative path starting with '..' or an absolute one.
@JOY
JOY (JOY) merged commit b8d7778 into dev Sep 21, 2026
9 of 10 checks passed
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.

4 participants