-
Notifications
You must be signed in to change notification settings - Fork 82
Feat : gitlab ci support #1061
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
ManilDf
wants to merge
32
commits into
matrix-org:main
Choose a base branch
from
madjidDer:feat/gitlab-ci-support
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Feat : gitlab ci support #1061
Changes from 12 commits
Commits
Show all changes
32 commits
Select commit
Hold shift + click to select a range
2d477e1
Support GitLab pipeline events
madjidDer 3b6f188
Tests
ManilDf f784f73
test: refactor pipeline event tests using loop
madjidDer 5595e18
ignore intermediate pipeline statuses
madjidDer 45b8bfa
Update pipeline tests and tweak message formatting
ManilDf b93b0bd
Add e2e Tests
ManilDf a7b9a06
handle CANCELED status and deduplicate notifications
ManilDf 34051aa
Merge branch 'main' into feat/gitlab-ci-support
ManilDf 13deb0a
docs: mention GitLab pipeline event in supported hooks
madjidDer 165cb88
Add changelog
madjidDer eeff431
remove comments
madjidDer 0559e53
Fix lint issues
madjidDer f960f23
Update changelog.d/1061.feature
ManilDf 92f24b5
Fix tests by adding missing type
ManilDf 2fe82a5
refactor: add first handler for pipeline.success
ManilDf 0112b90
Ping homeserver on startup and warn about configuration errors (#1062)
Half-Shot e708a23
Add option to omit the hook payload data from matrix events from gene…
Half-Shot 25364a6
Migrate GitHub, GitLab, and JIRA webhook path from `/` to `/<service>…
Half-Shot f00e17b
jira webhook docs fix
Half-Shot 8ad832c
Support GitLab pipeline events
madjidDer a25430c
refactor: add first handler for pipeline.success
ManilDf 503caa3
move gitlab pipeline mapping to Route.ts
Hakim-Lakrout 92c1419
Merge branch 'main' into feat/gitlab-ci-support
Hakim-Lakrout a46ae17
Fix lint issues
Hakim-Lakrout dd908ba
fix: issues and refactor code, address comments
ManilDf 37cef84
Update src/Connections/GitlabRepo.ts
Hakim-Lakrout 8ea8e71
Update src/gitlab/Router.ts
Hakim-Lakrout 090b849
Fix tsx format for pipeline and remove comment and duration when unused
Hakim-Lakrout 3874c7f
Add contributor
ManilDf ed5c542
Merge branch 'main' into feat/gitlab-ci-support
Hakim-Lakrout 81c5bff
Merge branch 'main' into feat/gitlab-ci-support
Hakim-Lakrout 839384c
Merge remote-tracking branch 'upstream/main' into feat/gitlab-ci-support
leguye File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| Adds support for GitLab 'pipeline' events via Hookshot. Thanks to @madjidDer, @ManilDF, and @leguye! | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,300 @@ | ||
| import { E2ESetupTestTimeout, E2ETestEnv } from "./util/e2e-test"; | ||
| import { describe, test, beforeAll, afterAll, expect } from "vitest"; | ||
| import { createHmac, randomUUID } from "crypto"; | ||
| import { | ||
| GitLabRepoConnection, | ||
| GitLabRepoConnectionState, | ||
| } from "../src/Connections"; | ||
| import { MessageEventContent } from "matrix-bot-sdk"; | ||
| import { getBridgeApi } from "./util/bridge-api"; | ||
| import { waitFor } from "./util/helpers"; | ||
| import { Server, createServer } from "http"; | ||
|
|
||
| describe("GitLab - Pipeline Event", () => { | ||
| let testEnv: E2ETestEnv; | ||
| let gitlabServer: Server; | ||
| const webhooksPort = 9801 + E2ETestEnv.workerId; | ||
| const gitlabPort = 9901 + E2ETestEnv.workerId; | ||
|
|
||
| beforeAll(async () => { | ||
| gitlabServer = createServer((req, res) => { | ||
| if (req.method === "GET" && req.url?.includes("/projects")) { | ||
| res.writeHead(200, { "Content-Type": "application/json" }); | ||
| res.end(JSON.stringify({ id: 1234 })); | ||
| } else { | ||
| console.log("Unknown GitLab request", req.method, req.url); | ||
| res.writeHead(404); | ||
| res.end(); | ||
| } | ||
| }).listen(gitlabPort); | ||
|
|
||
| testEnv = await E2ETestEnv.createTestEnv({ | ||
| matrixLocalparts: ["user"], | ||
| config: { | ||
| gitlab: { | ||
| webhook: { | ||
| secret: "mysecret", | ||
| }, | ||
| instances: { | ||
| test: { | ||
| url: `http://localhost:${gitlabPort}`, | ||
| }, | ||
| }, | ||
| }, | ||
| widgets: { | ||
| publicUrl: `http://localhost:${webhooksPort}`, | ||
| }, | ||
| listeners: [ | ||
| { | ||
| port: webhooksPort, | ||
| bindAddress: "0.0.0.0", | ||
| resources: ["webhooks", "widgets"], | ||
| }, | ||
| ], | ||
| }, | ||
| }); | ||
| await testEnv.setUp(); | ||
| }, E2ESetupTestTimeout); | ||
|
|
||
| afterAll(() => { | ||
| gitlabServer?.close(); | ||
| return testEnv?.tearDown(); | ||
| }); | ||
|
|
||
| const waitForMessages = ( | ||
| user: any, | ||
| roomId: string, | ||
| botMxid: string, | ||
| expectedCount: number, | ||
| timeoutMs: number = 10000, | ||
| ): Promise<MessageEventContent[]> => { | ||
| return new Promise((resolve, reject) => { | ||
| const receivedMessages: MessageEventContent[] = []; | ||
| const timeout = setTimeout(() => { | ||
| cleanup(); | ||
| reject( | ||
| new Error( | ||
| `Timeout: Expected ${expectedCount} messages, got ${receivedMessages.length}`, | ||
| ), | ||
| ); | ||
| }, timeoutMs); | ||
|
|
||
| const messageHandler = (eventRoomId: string, event: any) => { | ||
| if ( | ||
| eventRoomId === roomId && | ||
| event.sender === botMxid && | ||
| event.content?.msgtype === "m.notice" | ||
| ) { | ||
| receivedMessages.push(event.content); | ||
| if (receivedMessages.length >= expectedCount) { | ||
| cleanup(); | ||
| resolve(receivedMessages); | ||
| } | ||
| } | ||
| }; | ||
|
|
||
| const cleanup = () => { | ||
| clearTimeout(timeout); | ||
| user.off("room.message", messageHandler); | ||
| }; | ||
|
|
||
| user.on("room.message", messageHandler); | ||
| }); | ||
| }; | ||
|
|
||
| test( | ||
| "should handle GitLab pipeline success event with both messages", | ||
| async () => { | ||
| const user = testEnv.getUser("user"); | ||
| const bridgeApi = await getBridgeApi( | ||
| testEnv.opts.config?.widgets?.publicUrl!, | ||
| user, | ||
| ); | ||
| const testRoomId = await user.createRoom({ | ||
| name: "Pipeline Test Room", | ||
| invite: [testEnv.botMxid], | ||
| }); | ||
| await user.setUserPowerLevel(testEnv.botMxid, testRoomId, 50); | ||
| await user.waitForRoomJoin({ | ||
| sender: testEnv.botMxid, | ||
| roomId: testRoomId, | ||
| }); | ||
|
|
||
| await testEnv.app.appservice.botClient.sendStateEvent( | ||
| testRoomId, | ||
| GitLabRepoConnection.CanonicalEventType, | ||
| "my-test-pipeline", | ||
| { | ||
| instance: "test", | ||
| path: "org/project", | ||
| enableHooks: ["pipeline"], | ||
| } satisfies GitLabRepoConnectionState, | ||
| ); | ||
|
|
||
| await waitFor( | ||
| async () => | ||
| (await bridgeApi.getConnectionsForRoom(testRoomId)).length === 1, | ||
| ); | ||
|
|
||
| const messagesPromise = waitForMessages( | ||
| user, | ||
| testRoomId, | ||
| testEnv.botMxid, | ||
| 2, | ||
| ); | ||
|
|
||
| const webhookPayload = JSON.stringify({ | ||
| object_kind: "pipeline", | ||
| object_attributes: { | ||
| id: 123456, | ||
| status: "success", | ||
| ref: "main", | ||
| url: "https://gitlab.example.com/org/project/-/pipelines/123456", | ||
| duration: 300, | ||
| finished_at: "2025-01-01T12:00:00Z", | ||
| }, | ||
| project: { | ||
| id: 1234, | ||
| name: "project", | ||
| path_with_namespace: "org/project", | ||
| web_url: "https://gitlab.example.com/org/project", | ||
| }, | ||
| user: { | ||
| id: 1, | ||
| name: "Alice Doe", | ||
| username: "alice", | ||
| email: "[email protected]", | ||
| }, | ||
| commit: { | ||
| id: "abcd1234567890", | ||
| message: "Add new feature", | ||
| author_name: "Alice Doe", | ||
| author_email: "[email protected]", | ||
| }, | ||
| }); | ||
|
|
||
| const hmac = createHmac("sha256", "mysecret"); | ||
| hmac.write(webhookPayload); | ||
| hmac.end(); | ||
|
|
||
| const req = await fetch(`http://localhost:${webhooksPort}/`, { | ||
| method: "POST", | ||
| headers: { | ||
| "X-Gitlab-Event": "Pipeline Hook", | ||
| "X-Gitlab-Token": "mysecret", | ||
| "X-Hub-Signature-256": `sha256=${hmac.read().toString("hex")}`, | ||
| "Content-Type": "application/json", | ||
| }, | ||
| body: webhookPayload, | ||
| }); | ||
|
|
||
| expect(req.status).toBe(200); | ||
| expect(await req.text()).toBe("OK"); | ||
|
|
||
| const receivedMessages = await messagesPromise; | ||
|
|
||
| expect(receivedMessages.length).toBe(2); | ||
|
|
||
| const triggeredMessage = receivedMessages[0]; | ||
| expect(triggeredMessage.body.toLowerCase()).toContain("triggered"); | ||
|
|
||
| const successMessage = receivedMessages[1]; | ||
| expect(successMessage.body.toLowerCase()).toContain("success"); | ||
| }, | ||
| E2ESetupTestTimeout, | ||
| ); | ||
|
|
||
| test("should only send triggered message for running pipeline", async () => { | ||
| const user = testEnv.getUser("user"); | ||
| const bridgeApi = await getBridgeApi( | ||
| testEnv.opts.config?.widgets?.publicUrl!, | ||
| user, | ||
| ); | ||
| const testRoomId = await user.createRoom({ | ||
| name: "Pipeline Running Test Room", | ||
| invite: [testEnv.botMxid], | ||
| }); | ||
| await user.setUserPowerLevel(testEnv.botMxid, testRoomId, 50); | ||
| await user.waitForRoomJoin({ sender: testEnv.botMxid, roomId: testRoomId }); | ||
|
|
||
| await testEnv.app.appservice.botClient.sendStateEvent( | ||
| testRoomId, | ||
| GitLabRepoConnection.CanonicalEventType, | ||
| "my-test-pipeline-running", | ||
| { | ||
| instance: "test", | ||
| path: "org/project", | ||
| enableHooks: ["pipeline"], | ||
| } satisfies GitLabRepoConnectionState, | ||
| ); | ||
|
|
||
| await waitFor( | ||
| async () => | ||
| (await bridgeApi.getConnectionsForRoom(testRoomId)).length === 1, | ||
| ); | ||
|
|
||
| const receivedMessages: MessageEventContent[] = []; | ||
| const messageHandler = (roomId: string, event: any) => { | ||
| if (roomId === testRoomId && event.sender === testEnv.botMxid) { | ||
| receivedMessages.push(event.content); | ||
| } | ||
| }; | ||
| user.on("room.message", messageHandler); | ||
|
|
||
| const webhookPayload = JSON.stringify({ | ||
| object_kind: "pipeline", | ||
| object_attributes: { | ||
| id: 999888, | ||
| status: "running", | ||
| ref: "main", | ||
| url: "https://gitlab.example.com/org/project/-/pipelines/999888", | ||
| duration: null, | ||
| finished_at: null, | ||
| }, | ||
| project: { | ||
| id: 1234, | ||
| name: "project", | ||
| path_with_namespace: "org/project", | ||
| web_url: "https://gitlab.example.com/org/project", | ||
| }, | ||
| user: { | ||
| id: 4, | ||
| name: "David Wilson", | ||
| username: "david", | ||
| email: "[email protected]", | ||
| }, | ||
| commit: { | ||
| id: "mnop3456789012", | ||
| message: "Start new feature", | ||
| author_name: "David Wilson", | ||
| author_email: "[email protected]", | ||
| }, | ||
| }); | ||
|
|
||
| const hmac = createHmac("sha256", "mysecret"); | ||
| hmac.write(webhookPayload); | ||
| hmac.end(); | ||
|
|
||
| const req = await fetch(`http://localhost:${webhooksPort}/`, { | ||
| method: "POST", | ||
| headers: { | ||
| "X-Gitlab-Event": "Pipeline Hook", | ||
| "X-Gitlab-Token": "mysecret", | ||
| "X-Hub-Signature-256": `sha256=${hmac.read().toString("hex")}`, | ||
| "Content-Type": "application/json", | ||
| }, | ||
| body: webhookPayload, | ||
| }); | ||
|
|
||
| expect(req.status).toBe(200); | ||
| expect(await req.text()).toBe("OK"); | ||
|
|
||
| await waitFor(async () => receivedMessages.length >= 1, 3000); | ||
|
|
||
| await new Promise((resolve) => setTimeout(resolve, 1000)); | ||
|
|
||
| expect(receivedMessages.length).toBe(1); | ||
| const triggeredMessage = receivedMessages[0]; | ||
| expect(triggeredMessage.body.toLowerCase()).toContain("triggered"); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.