Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b31da5d315 | ||
|
|
deb01ec823 | ||
|
|
b53f1832d8 |
@@ -1,12 +1,12 @@
|
||||
name: Visual Regression Dispatch
|
||||
|
||||
# Uses workflow_run to dispatch visual regression to ci-privileged.
|
||||
# This runs in the context of the base repo (not the fork), so it has
|
||||
# access to secrets — making it work for external contributor PRs.
|
||||
# Dispatches visual regression processing to ci-privileged after CI completes.
|
||||
# Runs in the context of the base repo (not the fork) so it has access to secrets,
|
||||
# making it work for external contributor PRs.
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ['CI Front', 'CI UI']
|
||||
workflows: ['CI UI']
|
||||
types: [completed]
|
||||
|
||||
permissions:
|
||||
@@ -21,29 +21,28 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- name: Determine project and artifact name
|
||||
- name: Determine project
|
||||
id: project
|
||||
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0
|
||||
with:
|
||||
script: |
|
||||
const workflowName = context.payload.workflow_run.name;
|
||||
if (workflowName === 'CI Front') {
|
||||
core.setOutput('project', 'twenty-front');
|
||||
core.setOutput('artifact_name', 'storybook-static');
|
||||
core.setOutput('tarball_name', 'storybook-twenty-front-tarball');
|
||||
core.setOutput('tarball_file', 'storybook-twenty-front.tar.gz');
|
||||
core.setOutput('mode', 'storybook-tarball');
|
||||
} else if (workflowName === 'CI UI') {
|
||||
core.setOutput('project', 'twenty-ui');
|
||||
core.setOutput('artifact_name', 'argos-screenshots-twenty-ui');
|
||||
core.setOutput('tarball_name', 'argos-screenshots-twenty-ui-tarball');
|
||||
core.setOutput('tarball_file', 'argos-screenshots-twenty-ui.tar.gz');
|
||||
core.setOutput('mode', 'argos-screenshots');
|
||||
} else {
|
||||
const projects = {
|
||||
'CI UI': { project: 'twenty-ui', artifact: 'argos-screenshots-twenty-ui' },
|
||||
// Add more projects here when ready:
|
||||
// 'CI Front': { project: 'twenty-front', artifact: 'argos-screenshots-twenty-front' },
|
||||
};
|
||||
|
||||
const config = projects[workflowName];
|
||||
if (!config) {
|
||||
core.setFailed(`Unexpected workflow: ${workflowName}`);
|
||||
return;
|
||||
}
|
||||
|
||||
- name: Check if artifact exists
|
||||
core.setOutput('project', config.project);
|
||||
core.setOutput('artifact_name', config.artifact);
|
||||
|
||||
- name: Check if screenshots artifact exists
|
||||
id: check-artifact
|
||||
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0
|
||||
with:
|
||||
@@ -61,7 +60,7 @@ jobs:
|
||||
core.setOutput('exists', found ? 'true' : 'false');
|
||||
|
||||
if (!found) {
|
||||
core.info(`Artifact "${artifactName}" not found in run ${runId} — build was likely skipped`);
|
||||
core.info(`Artifact "${artifactName}" not found in run ${runId} — skipping`);
|
||||
}
|
||||
|
||||
- name: Get PR number
|
||||
@@ -73,8 +72,6 @@ jobs:
|
||||
const headBranch = context.payload.workflow_run.head_branch;
|
||||
const headRepo = context.payload.workflow_run.head_repository;
|
||||
|
||||
// workflow_run.pull_requests is empty for fork PRs,
|
||||
// so fall back to searching by head label (owner:branch)
|
||||
let pullRequests = context.payload.workflow_run.pull_requests;
|
||||
let prNumber;
|
||||
|
||||
@@ -82,7 +79,7 @@ jobs:
|
||||
prNumber = pullRequests[0].number;
|
||||
} else {
|
||||
const headLabel = `${headRepo.owner.login}:${headBranch}`;
|
||||
core.info(`pull_requests is empty (likely a fork PR), searching by head label: ${headLabel}`);
|
||||
core.info(`Searching for PR by head label: ${headLabel}`);
|
||||
|
||||
const { data: prs } = await github.rest.pulls.list({
|
||||
owner: context.repo.owner,
|
||||
@@ -98,7 +95,7 @@ jobs:
|
||||
}
|
||||
|
||||
if (!prNumber) {
|
||||
core.info('No pull request found for this workflow run — skipping');
|
||||
core.info('No pull request found — skipping');
|
||||
core.setOutput('has_pr', 'false');
|
||||
return;
|
||||
}
|
||||
@@ -107,45 +104,23 @@ jobs:
|
||||
core.setOutput('has_pr', 'true');
|
||||
core.info(`PR #${prNumber}`);
|
||||
|
||||
- name: Download artifact from triggering run
|
||||
if: steps.check-artifact.outputs.exists == 'true' && steps.pr-info.outputs.has_pr == 'true'
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
|
||||
with:
|
||||
name: ${{ steps.project.outputs.artifact_name }}
|
||||
path: artifact-content
|
||||
run-id: ${{ github.event.workflow_run.id }}
|
||||
github-token: ${{ github.token }}
|
||||
|
||||
- name: Package artifact as tarball
|
||||
if: steps.check-artifact.outputs.exists == 'true' && steps.pr-info.outputs.has_pr == 'true'
|
||||
run: tar -czf /tmp/${{ steps.project.outputs.tarball_file }} -C artifact-content .
|
||||
|
||||
- name: Upload tarball
|
||||
if: steps.check-artifact.outputs.exists == 'true' && steps.pr-info.outputs.has_pr == 'true'
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: ${{ steps.project.outputs.tarball_name }}
|
||||
path: /tmp/${{ steps.project.outputs.tarball_file }}
|
||||
retention-days: 1
|
||||
|
||||
- name: Dispatch to ci-privileged
|
||||
if: steps.check-artifact.outputs.exists == 'true' && steps.pr-info.outputs.has_pr == 'true'
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.CI_PRIVILEGED_DISPATCH_TOKEN }}
|
||||
PR_NUMBER: ${{ steps.pr-info.outputs.pr_number }}
|
||||
RUN_ID: ${{ github.run_id }}
|
||||
WORKFLOW_RUN_ID: ${{ github.event.workflow_run.id }}
|
||||
REPOSITORY: ${{ github.repository }}
|
||||
PROJECT: ${{ steps.project.outputs.project }}
|
||||
BRANCH: ${{ github.event.workflow_run.head_branch }}
|
||||
COMMIT: ${{ github.event.workflow_run.head_sha }}
|
||||
MODE: ${{ steps.project.outputs.mode }}
|
||||
run: |
|
||||
gh api repos/twentyhq/ci-privileged/dispatches \
|
||||
--method POST \
|
||||
-f event_type=visual-regression \
|
||||
-f "client_payload[pr_number]=$PR_NUMBER" \
|
||||
-f "client_payload[run_id]=$RUN_ID" \
|
||||
-f "client_payload[run_id]=$WORKFLOW_RUN_ID" \
|
||||
-f "client_payload[repo]=$REPOSITORY" \
|
||||
-f "client_payload[project]=$PROJECT" \
|
||||
-f "client_payload[branch]=$BRANCH" \
|
||||
-f "client_payload[commit]=$COMMIT" \
|
||||
-f "client_payload[mode]=$MODE"
|
||||
-f "client_payload[commit]=$COMMIT"
|
||||
|
||||
+12
-1
@@ -279,11 +279,22 @@ export class WorkspaceRolesPermissionsCacheService extends WorkspaceCacheProvide
|
||||
const hasPermissionFromSettingPermissions = isDefined(
|
||||
rolePermissionFlags.find(
|
||||
(rolePermissionFlag) =>
|
||||
rolePermissionFlag.permissionFlag.universalIdentifier ===
|
||||
this.getRolePermissionFlagUniversalIdentifier(rolePermissionFlag) ===
|
||||
permissionFlagUniversalIdentifier,
|
||||
),
|
||||
);
|
||||
|
||||
return hasPermissionFromRole || hasPermissionFromSettingPermissions;
|
||||
}
|
||||
|
||||
private getRolePermissionFlagUniversalIdentifier(
|
||||
rolePermissionFlag: RolePermissionFlagEntity,
|
||||
): string {
|
||||
// The `permissionFlag` relation is stripped during upgrades until the 2.6.0
|
||||
// cursor (@WasIntroducedInUpgrade), so fall back to the legacy `flag` column.
|
||||
return (
|
||||
rolePermissionFlag.permissionFlag?.universalIdentifier ??
|
||||
SystemPermissionFlag[rolePermissionFlag.flag]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+5
@@ -12,6 +12,7 @@ import {
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
|
||||
import { WasIntroducedInUpgrade } from 'src/engine/core-modules/upgrade/decorators/was-introduced-in-upgrade.decorator';
|
||||
import { type JsonbProperty } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/jsonb-property.type';
|
||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { ViewFilterGroupEntity } from 'src/engine/metadata-modules/view-filter-group/entities/view-filter-group.entity';
|
||||
@@ -64,6 +65,10 @@ export class ViewFilterEntity
|
||||
@Column({ nullable: true, type: 'text', default: null })
|
||||
subFieldName: string | null;
|
||||
|
||||
@WasIntroducedInUpgrade({
|
||||
upgradeCommandName:
|
||||
'2.6.0_AddRelationTargetFieldMetadataIdToViewFilterFastInstanceCommand_1798000005000',
|
||||
})
|
||||
@Column({ nullable: true, type: 'uuid', default: null })
|
||||
relationTargetFieldMetadataId: string | null;
|
||||
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
import { serializeJsonLd } from './serialize-json-ld';
|
||||
import type { JsonLdValue } from './types/json-ld-value';
|
||||
type JsonLdPrimitive = boolean | number | string | null;
|
||||
|
||||
export type JsonLdValue =
|
||||
| JsonLdPrimitive
|
||||
| JsonLdValue[]
|
||||
| { [key: string]: JsonLdValue | undefined };
|
||||
|
||||
const serializeJsonLd = (data: JsonLdValue): string =>
|
||||
JSON.stringify(data).replace(/</g, '\\u003c');
|
||||
|
||||
export function JsonLd({ data }: { data: JsonLdValue }) {
|
||||
return (
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
import { serializeJsonLd } from '@/lib/seo/serialize-json-ld';
|
||||
|
||||
describe('serializeJsonLd', () => {
|
||||
it('escapes <, > and / to their \\uXXXX form', () => {
|
||||
expect(serializeJsonLd({ value: '<>/' })).toBe(
|
||||
'{"value":"\\u003c\\u003e\\u002f"}',
|
||||
);
|
||||
});
|
||||
|
||||
it('neutralizes a </script> breakout attempt', () => {
|
||||
const result = serializeJsonLd({
|
||||
name: '</script><script>alert(1)</script>',
|
||||
});
|
||||
|
||||
expect(result).not.toContain('<');
|
||||
expect(result).not.toContain('>');
|
||||
expect(result).not.toContain('</script');
|
||||
});
|
||||
|
||||
it('round-trips: the escaped payload parses back to the original data', () => {
|
||||
const data = {
|
||||
'@context': 'https://schema.org',
|
||||
name: 'A & B </script>',
|
||||
nested: { items: ['<x>', '/path'] },
|
||||
};
|
||||
|
||||
expect(JSON.parse(serializeJsonLd(data))).toEqual(data);
|
||||
});
|
||||
});
|
||||
@@ -5,7 +5,7 @@ import type { Article } from '@/lib/articles';
|
||||
import { localeToUrlSegment } from '@/lib/i18n';
|
||||
import type { LocalReleaseNote } from '@/lib/releases/types';
|
||||
|
||||
import type { JsonLdValue } from './types/json-ld-value';
|
||||
import type { JsonLdValue } from './JsonLd';
|
||||
import { getSiteUrl } from './site-url';
|
||||
|
||||
type FaqEntryLike = {
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
import type { JsonLdValue } from './types/json-ld-value';
|
||||
|
||||
// Escape the script-significant characters to their \uXXXX form so the payload
|
||||
// can never break out of the <script> element (e.g. via </script>). Mirrors the
|
||||
// set used by serialize-javascript; they decode back when parsed as JSON.
|
||||
export const serializeJsonLd = (data: JsonLdValue): string =>
|
||||
JSON.stringify(data)
|
||||
.replace(/</g, '\\u003c')
|
||||
.replace(/>/g, '\\u003e')
|
||||
.replace(/\//g, '\\u002f');
|
||||
@@ -1,6 +0,0 @@
|
||||
type JsonLdPrimitive = boolean | number | string | null;
|
||||
|
||||
export type JsonLdValue =
|
||||
| JsonLdPrimitive
|
||||
| JsonLdValue[]
|
||||
| { [key: string]: JsonLdValue | undefined };
|
||||
Reference in New Issue
Block a user