From c83ad1faa78ab818548f4775149269386a2f0a06 Mon Sep 17 00:00:00 2001 From: Robin Date: Thu, 21 Apr 2022 07:41:38 -0400 Subject: [PATCH 1/3] Add local echo of connected devices in video rooms (#8368) --- src/components/views/rooms/RoomTile.tsx | 22 +++++--- src/components/views/voip/VideoLobby.tsx | 10 ++-- src/utils/VideoChannelUtils.ts | 38 ++++++++++---- test/components/views/rooms/RoomTile-test.tsx | 51 +++++++++++++++---- .../components/views/voip/VideoLobby-test.tsx | 45 ++++++++++++---- test/test-utils/test-utils.ts | 1 + test/test-utils/video.ts | 2 +- 7 files changed, 126 insertions(+), 43 deletions(-) diff --git a/src/components/views/rooms/RoomTile.tsx b/src/components/views/rooms/RoomTile.tsx index 530b22571aa..7b0a8e95de9 100644 --- a/src/components/views/rooms/RoomTile.tsx +++ b/src/components/views/rooms/RoomTile.tsx @@ -81,7 +81,7 @@ interface IState { messagePreview?: string; videoStatus: VideoStatus; // Active video channel members, according to room state - videoMembers: RoomMember[]; + videoMembers: Set; // Active video channel members, according to Jitsi jitsiParticipants: IJitsiParticipant[]; } @@ -124,7 +124,7 @@ export default class RoomTile extends React.PureComponent { // generatePreview() will return nothing if the user has previews disabled messagePreview: "", videoStatus, - videoMembers: getConnectedMembers(this.props.room.currentState), + videoMembers: getConnectedMembers(this.props.room, videoStatus === VideoStatus.Connected), jitsiParticipants: VideoChannelStore.instance.participants, }; this.generatePreview(); @@ -593,7 +593,9 @@ export default class RoomTile extends React.PureComponent { } private updateVideoMembers = () => { - this.setState({ videoMembers: getConnectedMembers(this.props.room.currentState) }); + this.setState(state => ({ + videoMembers: getConnectedMembers(this.props.room, state.videoStatus === VideoStatus.Connected), + })); }; private updateVideoStatus = () => { @@ -610,7 +612,10 @@ export default class RoomTile extends React.PureComponent { private onConnectVideo = (roomId: string) => { if (roomId === this.props.room?.roomId) { - this.setState({ videoStatus: VideoStatus.Connected }); + this.setState({ + videoStatus: VideoStatus.Connected, + videoMembers: getConnectedMembers(this.props.room, true), + }); VideoChannelStore.instance.on(VideoChannelEvent.Participants, this.updateJitsiParticipants); } }; @@ -623,7 +628,10 @@ export default class RoomTile extends React.PureComponent { private onDisconnectVideo = (roomId: string) => { if (roomId === this.props.room?.roomId) { - this.setState({ videoStatus: VideoStatus.Disconnected }); + this.setState({ + videoStatus: VideoStatus.Disconnected, + videoMembers: getConnectedMembers(this.props.room, false), + }); VideoChannelStore.instance.off(VideoChannelEvent.Participants, this.updateJitsiParticipants); } }; @@ -668,12 +676,12 @@ export default class RoomTile extends React.PureComponent { case VideoStatus.Disconnected: videoText = _t("Video"); videoActive = false; - participantCount = this.state.videoMembers.length; + participantCount = this.state.videoMembers.size; break; case VideoStatus.Connecting: videoText = _t("Connecting..."); videoActive = true; - participantCount = this.state.videoMembers.length; + participantCount = this.state.videoMembers.size; break; case VideoStatus.Connected: videoText = _t("Connected"); diff --git a/src/components/views/voip/VideoLobby.tsx b/src/components/views/voip/VideoLobby.tsx index 84bc470273e..f9e95089270 100644 --- a/src/components/views/voip/VideoLobby.tsx +++ b/src/components/views/voip/VideoLobby.tsx @@ -110,7 +110,7 @@ const MAX_FACES = 8; const VideoLobby: FC<{ room: Room }> = ({ room }) => { const [connecting, setConnecting] = useState(false); const me = useMemo(() => room.getMember(room.myUserId), [room]); - const connectedMembers = useConnectedMembers(room.currentState); + const connectedMembers = useConnectedMembers(room, false); const videoRef = useRef(); const devices = useAsyncMemo(async () => { @@ -172,12 +172,12 @@ const VideoLobby: FC<{ room: Room }> = ({ room }) => { }; let facePile; - if (connectedMembers.length) { - const shownMembers = connectedMembers.slice(0, MAX_FACES); - const overflow = connectedMembers.length > shownMembers.length; + if (connectedMembers.size) { + const shownMembers = [...connectedMembers].slice(0, MAX_FACES); + const overflow = connectedMembers.size > shownMembers.length; facePile =
- { _t("%(count)s people connected", { count: connectedMembers.length }) } + { _t("%(count)s people connected", { count: connectedMembers.size }) }
; } diff --git a/src/utils/VideoChannelUtils.ts b/src/utils/VideoChannelUtils.ts index 11a1a9a35f2..cc3c99d980c 100644 --- a/src/utils/VideoChannelUtils.ts +++ b/src/utils/VideoChannelUtils.ts @@ -17,7 +17,8 @@ limitations under the License. import { useState } from "react"; import { throttle } from "lodash"; import { CallType } from "matrix-js-sdk/src/webrtc/call"; -import { RoomState, RoomStateEvent } from "matrix-js-sdk/src/models/room-state"; +import { Room } from "matrix-js-sdk/src/models/room"; +import { RoomStateEvent } from "matrix-js-sdk/src/models/room-state"; import { RoomMember } from "matrix-js-sdk/src/models/room-member"; import { useTypedEventEmitter } from "../hooks/useEventEmitter"; @@ -42,17 +43,32 @@ export const addVideoChannel = async (roomId: string, roomName: string) => { await WidgetUtils.addJitsiWidget(roomId, CallType.Video, "Video channel", VIDEO_CHANNEL, roomName); }; -export const getConnectedMembers = (state: RoomState): RoomMember[] => - state.getStateEvents(VIDEO_CHANNEL_MEMBER) +export const getConnectedMembers = (room: Room, connectedLocalEcho: boolean): Set => { + const members = new Set(); + + for (const e of room.currentState.getStateEvents(VIDEO_CHANNEL_MEMBER)) { + const member = room.getMember(e.getStateKey()); + let devices = e.getContent()?.devices ?? []; + + // Apply local echo for the disconnected case + if (!connectedLocalEcho && member?.userId === room.client.getUserId()) { + devices = devices.filter(d => d !== room.client.getDeviceId()); + } // Must have a device connected and still be joined to the room - .filter(e => e.getContent()?.devices?.length) - .map(e => state.getMember(e.getStateKey())) - .filter(member => member?.membership === "join"); - -export const useConnectedMembers = (state: RoomState, throttleMs = 100) => { - const [members, setMembers] = useState(getConnectedMembers(state)); - useTypedEventEmitter(state, RoomStateEvent.Update, throttle(() => { - setMembers(getConnectedMembers(state)); + if (devices.length && member?.membership === "join") members.add(member); + } + + // Apply local echo for the connected case + if (connectedLocalEcho) members.add(room.getMember(room.client.getUserId())); + return members; +}; + +export const useConnectedMembers = ( + room: Room, connectedLocalEcho: boolean, throttleMs = 100, +): Set => { + const [members, setMembers] = useState>(getConnectedMembers(room, connectedLocalEcho)); + useTypedEventEmitter(room.currentState, RoomStateEvent.Update, throttle(() => { + setMembers(getConnectedMembers(room, connectedLocalEcho)); }, throttleMs, { leading: true, trailing: true })); return members; }; diff --git a/test/components/views/rooms/RoomTile-test.tsx b/test/components/views/rooms/RoomTile-test.tsx index d209c32f0f9..d07360f6d4f 100644 --- a/test/components/views/rooms/RoomTile-test.tsx +++ b/test/components/views/rooms/RoomTile-test.tsx @@ -18,6 +18,8 @@ import React from "react"; import { mount } from "enzyme"; import { act } from "react-dom/test-utils"; import { mocked } from "jest-mock"; +import { MatrixClient } from "matrix-js-sdk/src/client"; +import { Room } from "matrix-js-sdk/src/models/room"; import { RoomMember } from "matrix-js-sdk/src/models/room-member"; import { @@ -26,6 +28,7 @@ import { mkRoom, mkVideoChannelMember, stubVideoChannelStore, + StubVideoChannelStore, } from "../../../test-utils"; import RoomTile from "../../../../src/components/views/rooms/RoomTile"; import SettingsStore from "../../../../src/settings/SettingsStore"; @@ -39,9 +42,8 @@ describe("RoomTile", () => { jest.spyOn(PlatformPeg, 'get') .mockReturnValue({ overrideBrowserShortcuts: () => false } as unknown as BasePlatform); - let cli; - let store; - + let cli: MatrixClient; + let store: StubVideoChannelStore; beforeEach(() => { const realGetValue = SettingsStore.getValue; SettingsStore.getValue = (name: string, roomId?: string): T => { @@ -52,7 +54,7 @@ describe("RoomTile", () => { }; stubClient(); - cli = mocked(MatrixClientPeg.get()); + cli = MatrixClientPeg.get(); store = stubVideoChannelStore(); DMRoomMap.makeShared(); }); @@ -60,8 +62,11 @@ describe("RoomTile", () => { afterEach(() => jest.clearAllMocks()); describe("video rooms", () => { - const room = mkRoom(cli, "!1:example.org"); - room.isElementVideoRoom.mockReturnValue(true); + let room: Room; + beforeEach(() => { + room = mkRoom(cli, "!1:example.org"); + mocked(room.isElementVideoRoom).mockReturnValue(true); + }); it("tracks connection state", () => { const tile = mount( @@ -97,7 +102,7 @@ describe("RoomTile", () => { mkVideoChannelMember("@chris:example.org", ["device 1"]), ])); - mocked(room.currentState).getMember.mockImplementation(userId => ({ + mocked(room).getMember.mockImplementation(userId => ({ userId, membership: userId === "@chris:example.org" ? "leave" : "join", name: userId, @@ -117,8 +122,36 @@ describe("RoomTile", () => { ); // Only Alice should display as connected - const participants = tile.find(".mx_RoomTile_videoParticipants"); - expect(participants.text()).toEqual("1"); + expect(tile.find(".mx_RoomTile_videoParticipants").text()).toEqual("1"); + }); + + it("reflects local echo in connected members", () => { + mocked(room.currentState).getStateEvents.mockImplementation(mockStateEventImplementation([ + // Make the remote echo claim that we're connected, while leaving the store disconnected + mkVideoChannelMember(cli.getUserId(), [cli.getDeviceId()]), + ])); + + mocked(room).getMember.mockImplementation(userId => ({ + userId, + membership: "join", + name: userId, + rawDisplayName: userId, + roomId: "!1:example.org", + getAvatarUrl: () => {}, + getMxcAvatarUrl: () => {}, + }) as unknown as RoomMember); + + const tile = mount( + , + ); + + // Because of our local echo, we should still appear as disconnected + expect(tile.find(".mx_RoomTile_videoParticipants").exists()).toEqual(false); }); }); }); diff --git a/test/components/views/voip/VideoLobby-test.tsx b/test/components/views/voip/VideoLobby-test.tsx index 4e7afb12c44..2d69709dc76 100644 --- a/test/components/views/voip/VideoLobby-test.tsx +++ b/test/components/views/voip/VideoLobby-test.tsx @@ -18,11 +18,14 @@ import React from "react"; import { mount } from "enzyme"; import { act } from "react-dom/test-utils"; import { mocked } from "jest-mock"; +import { MatrixClient } from "matrix-js-sdk/src/client"; +import { Room } from "matrix-js-sdk/src/models/room"; import { RoomMember } from "matrix-js-sdk/src/models/room-member"; import { stubClient, stubVideoChannelStore, + StubVideoChannelStore, mkRoom, mkVideoChannelMember, mockStateEventImplementation, @@ -33,7 +36,6 @@ import MemberAvatar from "../../../../src/components/views/avatars/MemberAvatar" import VideoLobby from "../../../../src/components/views/voip/VideoLobby"; describe("VideoLobby", () => { - stubClient(); Object.defineProperty(navigator, "mediaDevices", { value: { enumerateDevices: jest.fn(), @@ -42,19 +44,17 @@ describe("VideoLobby", () => { }); jest.spyOn(HTMLMediaElement.prototype, "play").mockImplementation(async () => {}); - const cli = MatrixClientPeg.get(); - const room = mkRoom(cli, "!1:example.org"); - - let store; + let cli: MatrixClient; + let store: StubVideoChannelStore; + let room: Room; beforeEach(() => { + stubClient(); + cli = MatrixClientPeg.get(); store = stubVideoChannelStore(); + room = mkRoom(cli, "!1:example.org"); mocked(navigator.mediaDevices.enumerateDevices).mockResolvedValue([]); }); - afterEach(() => { - jest.clearAllMocks(); - }); - describe("connected members", () => { it("hides when no one is connected", async () => { const lobby = mount(); @@ -75,7 +75,7 @@ describe("VideoLobby", () => { mkVideoChannelMember("@chris:example.org", ["device 1"]), ])); - mocked(room.currentState).getMember.mockImplementation(userId => ({ + mocked(room).getMember.mockImplementation(userId => ({ userId, membership: userId === "@chris:example.org" ? "leave" : "join", name: userId, @@ -95,6 +95,31 @@ describe("VideoLobby", () => { expect(memberText).toEqual("1 person connected"); expect(lobby.find(FacePile).find(MemberAvatar).props().member.userId).toEqual("@alice:example.org"); }); + + it("doesn't include remote echo of this device being connected", async () => { + mocked(room.currentState).getStateEvents.mockImplementation(mockStateEventImplementation([ + // Make the remote echo claim that we're connected, while leaving the store disconnected + mkVideoChannelMember(cli.getUserId(), [cli.getDeviceId()]), + ])); + + mocked(room).getMember.mockImplementation(userId => ({ + userId, + membership: "join", + name: userId, + rawDisplayName: userId, + roomId: "!1:example.org", + getAvatarUrl: () => {}, + getMxcAvatarUrl: () => {}, + }) as unknown as RoomMember); + + const lobby = mount(); + // Wait for state to settle + await act(() => Promise.resolve()); + lobby.update(); + + // Because of our local echo, we should still appear as disconnected + expect(lobby.find(".mx_VideoLobby_connectedMembers").exists()).toEqual(false); + }); }); describe("device buttons", () => { diff --git a/test/test-utils/test-utils.ts b/test/test-utils/test-utils.ts index fc85a825f31..a590474ffed 100644 --- a/test/test-utils/test-utils.ts +++ b/test/test-utils/test-utils.ts @@ -379,6 +379,7 @@ export function mkStubRoom(roomId: string = null, name: string, client: MatrixCl getJoinRule: jest.fn().mockReturnValue("invite"), loadMembersIfNeeded: jest.fn(), client, + myUserId: client?.getUserId(), canInvite: jest.fn(), } as unknown as Room; } diff --git a/test/test-utils/video.ts b/test/test-utils/video.ts index 79c657a0c60..77fdfb8fcc0 100644 --- a/test/test-utils/video.ts +++ b/test/test-utils/video.ts @@ -21,7 +21,7 @@ import { mkEvent } from "./test-utils"; import { VIDEO_CHANNEL_MEMBER } from "../../src/utils/VideoChannelUtils"; import VideoChannelStore, { VideoChannelEvent, IJitsiParticipant } from "../../src/stores/VideoChannelStore"; -class StubVideoChannelStore extends EventEmitter { +export class StubVideoChannelStore extends EventEmitter { private _roomId: string; public get roomId(): string { return this._roomId; } private _connected: boolean; From dd880df6ae07300e77ad0fef96bb0378ac6ea09d Mon Sep 17 00:00:00 2001 From: Robin Date: Thu, 21 Apr 2022 07:41:58 -0400 Subject: [PATCH 2/3] Forcefully disconnect from video rooms on logout and tab close (#8375) * Forcefully disconnect from video rooms on logout * Forcefully disconnect from video rooms on tab close --- src/components/structures/MatrixChat.tsx | 2 ++ src/stores/VideoChannelStore.ts | 41 +++++++++++++----------- 2 files changed, 25 insertions(+), 18 deletions(-) diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index 624909db31d..328af853142 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -131,6 +131,7 @@ import { IConfigOptions } from "../../IConfigOptions"; import { SnakedObject } from "../../utils/SnakedObject"; import InfoDialog from '../views/dialogs/InfoDialog'; import { leaveRoomBehaviour } from "../../utils/leave-behaviour"; +import VideoChannelStore from "../../stores/VideoChannelStore"; // legacy export export { default as Views } from "../../Views"; @@ -576,6 +577,7 @@ export default class MatrixChat extends React.PureComponent { break; case 'logout': CallHandler.instance.hangupAllCalls(); + if (VideoChannelStore.instance.connected) VideoChannelStore.instance.setDisconnected(); Lifecycle.logout(); break; case 'require_registration': diff --git a/src/stores/VideoChannelStore.ts b/src/stores/VideoChannelStore.ts index 58e18cab985..14016800487 100644 --- a/src/stores/VideoChannelStore.ts +++ b/src/stores/VideoChannelStore.ts @@ -171,6 +171,7 @@ export default class VideoChannelStore extends AsyncStoreWithClient { this.connected = true; messaging.once(`action:${ElementWidgetActions.HangupCall}`, this.onHangup); + window.addEventListener("beforeunload", this.setDisconnected); this.emit(VideoChannelEvent.Connect, roomId); @@ -190,6 +191,27 @@ export default class VideoChannelStore extends AsyncStoreWithClient { } }; + public setDisconnected = async () => { + this.activeChannel.off(`action:${ElementWidgetActions.HangupCall}`, this.onHangup); + this.activeChannel.off(`action:${ElementWidgetActions.CallParticipants}`, this.onParticipants); + window.removeEventListener("beforeunload", this.setDisconnected); + + const roomId = this.roomId; + this.activeChannel = null; + this.roomId = null; + this.connected = false; + this.participants = []; + + this.emit(VideoChannelEvent.Disconnect, roomId); + + // Tell others that we're disconnected, by removing our device from room state + await this.updateDevices(roomId, devices => { + const devicesSet = new Set(devices); + devicesSet.delete(this.matrixClient.getDeviceId()); + return Array.from(devicesSet); + }); + }; + private ack = (ev: CustomEvent) => { // Even if we don't have a reply to a given widget action, we still need // to give the widget API something to acknowledge receipt @@ -208,24 +230,7 @@ export default class VideoChannelStore extends AsyncStoreWithClient { private onHangup = async (ev: CustomEvent) => { this.ack(ev); - - this.activeChannel.off(`action:${ElementWidgetActions.HangupCall}`, this.onHangup); - this.activeChannel.off(`action:${ElementWidgetActions.CallParticipants}`, this.onParticipants); - - const roomId = this.roomId; - this.activeChannel = null; - this.roomId = null; - this.connected = false; - this.participants = []; - - this.emit(VideoChannelEvent.Disconnect, roomId); - - // Tell others that we're disconnected, by removing our device from room state - await this.updateDevices(roomId, devices => { - const devicesSet = new Set(devices); - devicesSet.delete(this.matrixClient.getDeviceId()); - return Array.from(devicesSet); - }); + await this.setDisconnected(); }; private onParticipants = (ev: CustomEvent) => { From 146bcdd6a6314fef883a3569a9e95989f3857818 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 21 Apr 2022 12:55:32 +0100 Subject: [PATCH 3/3] Move more stuff from BK to GHA (#8372) --- .editorconfig | 3 + .github/workflows/element-build-and-test.yaml | 126 +++++++++------ .github/workflows/end-to-end-tests.yaml | 99 ++++++------ .github/workflows/netlify.yaml | 145 +++++++++--------- .github/workflows/notify-element-web.yml | 27 ++-- .github/workflows/preview_changelog.yaml | 14 +- .github/workflows/static_analysis.yaml | 88 +++++++++++ .../{test_coverage.yml => tests.yml} | 17 +- .github/workflows/typecheck.yaml | 27 ---- 9 files changed, 336 insertions(+), 210 deletions(-) create mode 100644 .github/workflows/static_analysis.yaml rename .github/workflows/{test_coverage.yml => tests.yml} (78%) delete mode 100644 .github/workflows/typecheck.yaml diff --git a/.editorconfig b/.editorconfig index 880331a09e5..56631484cd5 100644 --- a/.editorconfig +++ b/.editorconfig @@ -21,3 +21,6 @@ insert_final_newline = true indent_style = space indent_size = 4 trim_trailing_whitespace = true + +[*.{yml,yaml}] +indent_size = 2 diff --git a/.github/workflows/element-build-and-test.yaml b/.github/workflows/element-build-and-test.yaml index 1633aae2609..905dbedb067 100644 --- a/.github/workflows/element-build-and-test.yaml +++ b/.github/workflows/element-build-and-test.yaml @@ -3,47 +3,87 @@ # as an artifact and run integration tests. name: Element Web - Build and Test on: - pull_request: + pull_request: { } + push: + branches: [ develop, master ] + repository_dispatch: + types: [ upstream-sdk-notify ] jobs: - build: - runs-on: ubuntu-latest - env: - # This must be set for fetchdep.sh to get the right branch - PR_NUMBER: ${{github.event.number}} - steps: - - uses: actions/checkout@v2 - - name: Build - run: scripts/ci/layered.sh && cd element-web && cp element.io/develop/config.json config.json && CI_PACKAGE=true yarn build - - name: Upload Artifact - uses: actions/upload-artifact@v2 - with: - name: previewbuild - path: element-web/webapp - # We'll only use this in a triggered job, then we're done with it - retention-days: 1 - cypress: - needs: build - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - name: Download build - uses: actions/download-artifact@v3 - with: - name: previewbuild - path: webapp - - name: Run Cypress tests - uses: cypress-io/github-action@v2 - with: - # The built in Electron runner seems to grind to a halt trying - # to run the tests, so use chrome. - browser: chrome - start: npx serve -p 8080 webapp - - name: Upload Artifact - if: failure() - uses: actions/upload-artifact@v2 - with: - name: cypress-results - path: | - cypress/screenshots - cypress/videos - cypress/synapselogs + build: + name: "Build Element-Web" + runs-on: ubuntu-latest + env: + # This must be set for fetchdep.sh to get the right branch + PR_NUMBER: ${{github.event.number}} + steps: + - uses: actions/checkout@v2 + + - uses: actions/setup-node@v3 + with: + cache: 'yarn' + + - name: Fetch layered build + run: scripts/ci/layered.sh + + - name: Copy config + run: cp element.io/develop/config.json config.json + working-directory: ./element-web + + - name: Build + run: CI_PACKAGE=true yarn build + working-directory: ./element-web + + - name: Upload Artifact + uses: actions/upload-artifact@v2 + with: + name: previewbuild + path: element-web/webapp + # We'll only use this in a triggered job, then we're done with it + retention-days: 1 + + cypress: + name: "Cypress End to End Tests" + needs: build + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - name: Download build + uses: actions/download-artifact@v3 + with: + name: previewbuild + path: webapp + + - name: Run Cypress tests + uses: cypress-io/github-action@v2 + with: + # The built in Electron runner seems to grind to a halt trying + # to run the tests, so use chrome. + browser: chrome + start: npx serve -p 8080 webapp + + - name: Upload Artifact + if: failure() + uses: actions/upload-artifact@v2 + with: + name: cypress-results + path: | + cypress/screenshots + cypress/videos + cypress/synapselogs + + app-tests: + name: Element Web Integration Tests + runs-on: ubuntu-latest + env: + # This must be set for fetchdep.sh to get the right branch + PR_NUMBER: ${{github.event.number}} + steps: + - uses: actions/checkout@v2 + + - uses: actions/setup-node@v3 + with: + cache: 'yarn' + + - name: Run tests + run: "./scripts/ci/app-tests.sh" diff --git a/.github/workflows/end-to-end-tests.yaml b/.github/workflows/end-to-end-tests.yaml index 334af1772fd..1feaf266e36 100644 --- a/.github/workflows/end-to-end-tests.yaml +++ b/.github/workflows/end-to-end-tests.yaml @@ -1,47 +1,58 @@ name: End-to-end Tests on: - # These tests won't work for non-develop branches at the moment as they - # won't pull in the right versions of other repos, so they're only enabled - # on develop. - push: - branches: [develop] - pull_request: - branches: [develop] + # These tests won't work for non-develop branches at the moment as they + # won't pull in the right versions of other repos, so they're only enabled + # on develop. + push: + branches: [ develop ] + pull_request: + branches: [ develop ] + repository_dispatch: + types: [ upstream-sdk-notify ] jobs: - end-to-end: - runs-on: ubuntu-latest - env: - # This must be set for fetchdep.sh to get the right branch - PR_NUMBER: ${{github.event.number}} - container: vectorim/element-web-ci-e2etests-env:latest - steps: - - name: Checkout code - uses: actions/checkout@v2 - - name: Prepare End-to-End tests - run: ./scripts/ci/prepare-end-to-end-tests.sh - - name: Run End-to-End tests - run: ./scripts/ci/run-end-to-end-tests.sh - - name: Archive logs - uses: actions/upload-artifact@v2 - if: ${{ always() }} - with: - path: | - test/end-to-end-tests/logs/**/* - test/end-to-end-tests/synapse/installations/consent/homeserver.log - retention-days: 14 - - name: Download previous benchmark data - uses: actions/cache@v1 - with: - path: ./cache - key: ${{ runner.os }}-benchmark - - name: Store benchmark result - uses: matrix-org/github-action-benchmark@jsperfentry-1 - with: - tool: 'jsperformanceentry' - output-file-path: test/end-to-end-tests/performance-entries.json - fail-on-alert: false - comment-on-alert: false - # Only temporary to monitor where failures occur - alert-comment-cc-users: '@gsouquet' - github-token: ${{ secrets.DEPLOY_GH_PAGES }} - auto-push: ${{ github.ref == 'refs/heads/develop' }} + end-to-end: + runs-on: ubuntu-latest + env: + # This must be set for fetchdep.sh to get the right branch + PR_NUMBER: ${{github.event.number}} + container: vectorim/element-web-ci-e2etests-env:latest + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - uses: actions/setup-node@v3 + with: + cache: 'yarn' + + - name: Prepare End-to-End tests + run: ./scripts/ci/prepare-end-to-end-tests.sh + + - name: Run End-to-End tests + run: ./scripts/ci/run-end-to-end-tests.sh + + - name: Archive logs + uses: actions/upload-artifact@v2 + if: ${{ always() }} + with: + path: | + test/end-to-end-tests/logs/**/* + test/end-to-end-tests/synapse/installations/consent/homeserver.log + retention-days: 14 + + - name: Download previous benchmark data + uses: actions/cache@v1 + with: + path: ./cache + key: ${{ runner.os }}-benchmark + + - name: Store benchmark result + uses: matrix-org/github-action-benchmark@jsperfentry-1 + with: + tool: 'jsperformanceentry' + output-file-path: test/end-to-end-tests/performance-entries.json + fail-on-alert: false + comment-on-alert: false + # Only temporary to monitor where failures occur + alert-comment-cc-users: '@gsouquet' + github-token: ${{ secrets.DEPLOY_GH_PAGES }} + auto-push: ${{ github.ref == 'refs/heads/develop' }} diff --git a/.github/workflows/netlify.yaml b/.github/workflows/netlify.yaml index ec09379b6e3..1acb7e8fd14 100644 --- a/.github/workflows/netlify.yaml +++ b/.github/workflows/netlify.yaml @@ -2,76 +2,79 @@ # and uploading it to netlify name: Upload Preview Build to Netlify on: - workflow_run: - workflows: ["Element Web - Build and Test"] - types: - - completed + workflow_run: + workflows: [ "Element Web - Build and Test" ] + types: + - completed jobs: - build: - runs-on: ubuntu-latest - if: > - ${{ github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event == 'pull_request' }} - steps: - - name: "🔍 Read PR number" - id: readctx - # we need to find the PR number that corresponds to the branch, which we do by - # searching the GH API - # The workflow_run event includes a list of pull requests, but it doesn't get populated for - # forked PRs: https://docs.github.com/en/rest/reference/checks#create-a-check-run - run: | - head_branch='${{github.event.workflow_run.head_repository.owner.login}}:${{github.event.workflow_run.head_branch}}' - echo "head branch: $head_branch" - pulls_uri="https://api.github.com/repos/${{ github.repository }}/pulls?head=$(jq -Rr '@uri' <<<$head_branch)" - pr_number=$(curl -s -H 'Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}' "$pulls_uri" | - jq -r '.[] | .number') - echo "PR number: $pr_number" - echo "::set-output name=prnumber::$pr_number" - # There's a 'download artifact' action but it hasn't been updated for the - # workflow_run action (https://github.com/actions/download-artifact/issues/60) - # so instead we get this mess: - - name: 'Download artifact' - uses: actions/github-script@v3.1.0 - with: - script: | - var artifacts = await github.actions.listWorkflowRunArtifacts({ - owner: context.repo.owner, - repo: context.repo.repo, - run_id: ${{github.event.workflow_run.id }}, - }); - var matchArtifact = artifacts.data.artifacts.filter((artifact) => { - return artifact.name == "previewbuild" - })[0]; - var download = await github.actions.downloadArtifact({ - owner: context.repo.owner, - repo: context.repo.repo, - artifact_id: matchArtifact.id, - archive_format: 'zip', - }); - var fs = require('fs'); - fs.writeFileSync('${{github.workspace}}/previewbuild.zip', Buffer.from(download.data)); - - name: Extract Artifacts - run: unzip -d webapp previewbuild.zip && rm previewbuild.zip - - name: Deploy to Netlify - id: netlify - uses: nwtgck/actions-netlify@v1.2 - with: - publish-dir: webapp - deploy-message: "Deploy from GitHub Actions" - # These don't work because we're in workflow_run - enable-pull-request-comment: false - enable-commit-comment: false - alias: pr${{ steps.readctx.outputs.prnumber }} - env: - NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }} - NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }} - timeout-minutes: 1 - - name: Edit PR Description - uses: Beakyn/gha-comment-pull-request@2167a7aee24f9e61ce76a23039f322e49a990409 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - pull-request-number: ${{ steps.readctx.outputs.prnumber }} - description-message: | - Preview: ${{ steps.netlify.outputs.deploy-url }} - ⚠️ Do you trust the author of this PR? Maybe this build will steal your keys or give you malware. Exercise caution. Use test accounts. + build: + runs-on: ubuntu-latest + if: > + ${{ github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event == 'pull_request' }} + steps: + - name: "🔍 Read PR number" + id: readctx + # we need to find the PR number that corresponds to the branch, which we do by + # searching the GH API + # The workflow_run event includes a list of pull requests, but it doesn't get populated for + # forked PRs: https://docs.github.com/en/rest/reference/checks#create-a-check-run + run: | + head_branch='${{github.event.workflow_run.head_repository.owner.login}}:${{github.event.workflow_run.head_branch}}' + echo "head branch: $head_branch" + pulls_uri="https://api.github.com/repos/${{ github.repository }}/pulls?head=$(jq -Rr '@uri' <<<$head_branch)" + pr_number=$(curl -s -H 'Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}' "$pulls_uri" | + jq -r '.[] | .number') + echo "PR number: $pr_number" + echo "::set-output name=prnumber::$pr_number" + # There's a 'download artifact' action but it hasn't been updated for the + # workflow_run action (https://github.com/actions/download-artifact/issues/60) + # so instead we get this mess: + - name: 'Download artifact' + uses: actions/github-script@v3.1.0 + with: + script: | + var artifacts = await github.actions.listWorkflowRunArtifacts({ + owner: context.repo.owner, + repo: context.repo.repo, + run_id: ${{github.event.workflow_run.id }}, + }); + var matchArtifact = artifacts.data.artifacts.filter((artifact) => { + return artifact.name == "previewbuild" + })[0]; + var download = await github.actions.downloadArtifact({ + owner: context.repo.owner, + repo: context.repo.repo, + artifact_id: matchArtifact.id, + archive_format: 'zip', + }); + var fs = require('fs'); + fs.writeFileSync('${{github.workspace}}/previewbuild.zip', Buffer.from(download.data)); + + - name: Extract Artifacts + run: unzip -d webapp previewbuild.zip && rm previewbuild.zip + + - name: Deploy to Netlify + id: netlify + uses: nwtgck/actions-netlify@v1.2 + with: + publish-dir: webapp + deploy-message: "Deploy from GitHub Actions" + # These don't work because we're in workflow_run + enable-pull-request-comment: false + enable-commit-comment: false + alias: pr${{ steps.readctx.outputs.prnumber }} + env: + NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }} + NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }} + timeout-minutes: 1 + + - name: Edit PR Description + uses: Beakyn/gha-comment-pull-request@2167a7aee24f9e61ce76a23039f322e49a990409 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + pull-request-number: ${{ steps.readctx.outputs.prnumber }} + description-message: | + Preview: ${{ steps.netlify.outputs.deploy-url }} + ⚠️ Do you trust the author of this PR? Maybe this build will steal your keys or give you malware. Exercise caution. Use test accounts. diff --git a/.github/workflows/notify-element-web.yml b/.github/workflows/notify-element-web.yml index ef463784f38..c5c89905ced 100644 --- a/.github/workflows/notify-element-web.yml +++ b/.github/workflows/notify-element-web.yml @@ -1,15 +1,18 @@ name: Notify element-web on: - push: - branches: [develop] + push: + branches: [ develop ] + repository_dispatch: + types: [ upstream-sdk-notify ] jobs: - notify-element-web: - runs-on: ubuntu-latest - environment: develop - steps: - - name: Notify element-web repo that a new SDK build is on develop - uses: peter-evans/repository-dispatch@v1 - with: - token: ${{ secrets.ELEMENT_WEB_NOTIFY_TOKEN }} - repository: vector-im/element-web - event-type: element-web-notify + notify-element-web: + name: "Notify Element Web" + runs-on: ubuntu-latest + environment: develop + steps: + - name: Notify element-web repo that a new SDK build is on develop + uses: peter-evans/repository-dispatch@v1 + with: + token: ${{ secrets.ELEMENT_BOT_TOKEN }} + repository: vector-im/element-web + event-type: element-web-notify diff --git a/.github/workflows/preview_changelog.yaml b/.github/workflows/preview_changelog.yaml index d68d19361da..786d828d419 100644 --- a/.github/workflows/preview_changelog.yaml +++ b/.github/workflows/preview_changelog.yaml @@ -3,10 +3,10 @@ on: pull_request_target: types: [ opened, edited, labeled ] jobs: - changelog: - runs-on: ubuntu-latest - steps: - - name: Preview Changelog - uses: matrix-org/allchange@main - with: - ghToken: ${{ secrets.GITHUB_TOKEN }} + changelog: + runs-on: ubuntu-latest + steps: + - name: Preview Changelog + uses: matrix-org/allchange@main + with: + ghToken: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/static_analysis.yaml b/.github/workflows/static_analysis.yaml new file mode 100644 index 00000000000..8e320d99920 --- /dev/null +++ b/.github/workflows/static_analysis.yaml @@ -0,0 +1,88 @@ +name: Static Analysis +on: + pull_request: { } + push: + branches: [ develop, master ] + repository_dispatch: + types: [ upstream-sdk-notify ] +jobs: + ts_lint: + name: "Typescript Syntax Check" + runs-on: ubuntu-latest + env: + # This must be set for fetchdep.sh to get the right branch + PR_NUMBER: ${{github.event.number}} + steps: + - uses: actions/checkout@v2 + + - uses: actions/setup-node@v3 + with: + cache: 'yarn' + + - name: Install Deps + run: "./scripts/ci/install-deps.sh --ignore-scripts" + + - name: Typecheck + run: "yarn run lint:types" + + - name: Switch js-sdk to release mode + run: | + scripts/ci/js-sdk-to-release.js + cd node_modules/matrix-js-sdk + yarn install + yarn run build:compile + yarn run build:types + + - name: Typecheck (release mode) + run: "yarn run lint:types" + + i18n_lint: + name: "i18n Diff Check" + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - uses: actions/setup-node@v3 + with: + cache: 'yarn' + + # Does not need branch matching as only analyses this layer + - name: Install Deps + run: "yarn install" + + - name: i18n Check + run: "yarn run diff-i18n" + + js_lint: + name: "ESLint" + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - uses: actions/setup-node@v3 + with: + cache: 'yarn' + + # Does not need branch matching as only analyses this layer + - name: Install Deps + run: "yarn install" + + - name: Run Linter + run: "yarn run lint:js" + + style_lint: + name: "Style Lint" + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - uses: actions/setup-node@v3 + with: + cache: 'yarn' + + # Does not need branch matching as only analyses this layer + - name: Install Deps + run: "yarn install" + + - name: Run Linter + run: "yarn run lint:style" diff --git a/.github/workflows/test_coverage.yml b/.github/workflows/tests.yml similarity index 78% rename from .github/workflows/test_coverage.yml rename to .github/workflows/tests.yml index 4cd9f6d2f06..dc11981b7cf 100644 --- a/.github/workflows/test_coverage.yml +++ b/.github/workflows/tests.yml @@ -1,10 +1,13 @@ -name: Test coverage +name: Tests on: - pull_request: {} + pull_request: { } push: - branches: [develop, main, master] + branches: [ develop, master ] + repository_dispatch: + types: [ upstream-sdk-notify ] jobs: - test-coverage: + jest: + name: Jest with Codecov runs-on: ubuntu-latest env: # This must be set for fetchdep.sh to get the right branch @@ -19,13 +22,15 @@ jobs: ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || '' }} - name: Yarn cache - uses: c-hive/gha-yarn-cache@v2 + uses: actions/setup-node@v3 + with: + cache: 'yarn' - name: Install Deps run: "./scripts/ci/install-deps.sh --ignore-scripts" - name: Run tests with coverage - run: "yarn install && yarn coverage" + run: "yarn coverage" - name: Upload coverage uses: codecov/codecov-action@v2 diff --git a/.github/workflows/typecheck.yaml b/.github/workflows/typecheck.yaml deleted file mode 100644 index 60cabb3caba..00000000000 --- a/.github/workflows/typecheck.yaml +++ /dev/null @@ -1,27 +0,0 @@ -name: Type Check -on: - pull_request: - branches: [develop] -jobs: - build: - runs-on: ubuntu-latest - env: - # This must be set for fetchdep.sh to get the right branch - PR_NUMBER: ${{github.event.number}} - steps: - - uses: actions/checkout@v2 - - uses: c-hive/gha-yarn-cache@v2 - - name: Install Deps - run: "./scripts/ci/install-deps.sh --ignore-scripts" - - name: Typecheck - run: "yarn run lint:types" - - name: Switch js-sdk to release mode - run: | - scripts/ci/js-sdk-to-release.js - cd node_modules/matrix-js-sdk - yarn install - yarn run build:compile - yarn run build:types - - name: Typecheck (release mode) - run: "yarn run lint:types" -