Skip to content

Commit 333ca27

Browse files
committed
Merge remote-tracking branch 'origin/main' into refactor-proxy-module-ts-and-esm
2 parents cecfc7f + 9046dd4 commit 333ca27

File tree

16 files changed

+527
-74
lines changed

16 files changed

+527
-74
lines changed

cypress/e2e/autoApproved.cy.js

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import moment from 'moment';
2+
3+
describe('Auto-Approved Push Test', () => {
4+
beforeEach(() => {
5+
cy.intercept('GET', '/api/v1/push/123', {
6+
statusCode: 200,
7+
body: {
8+
steps: [
9+
{
10+
stepName: 'diff',
11+
content: '',
12+
},
13+
],
14+
error: false,
15+
allowPush: true,
16+
authorised: true,
17+
canceled: false,
18+
rejected: false,
19+
autoApproved: true,
20+
autoRejected: false,
21+
commitFrom: 'commitFrom',
22+
commitTo: 'commitTo',
23+
branch: 'refs/heads/main',
24+
user: 'testUser',
25+
id: 'commitFrom__commitTo',
26+
type: 'push',
27+
method: 'POST',
28+
timestamp: 1696161600000,
29+
project: 'testUser',
30+
repoName: 'test.git',
31+
url: 'https://github.com/testUser/test.git',
32+
repo: 'testUser/test.git',
33+
commitData: [
34+
{
35+
tree: '1234',
36+
parent: '12345',
37+
},
38+
],
39+
attestation: {
40+
timestamp: '2023-10-01T12:00:00Z',
41+
autoApproved: true,
42+
},
43+
},
44+
}).as('getPush');
45+
});
46+
47+
it('should display auto-approved message and verify tooltip contains the expected timestamp', () => {
48+
cy.visit('/admin/push/123');
49+
50+
cy.wait('@getPush');
51+
52+
cy.contains('Auto-approved by system').should('be.visible');
53+
54+
cy.get('svg.MuiSvgIcon-root')
55+
.filter((_, el) => getComputedStyle(el).fill === 'rgb(0, 128, 0)')
56+
.invoke('attr', 'style')
57+
.should('include', 'cursor: default')
58+
.and('include', 'opacity: 0.5');
59+
60+
const expectedTooltipTimestamp = moment('2023-10-01T12:00:00Z')
61+
.local()
62+
.format('dddd, MMMM Do YYYY, h:mm:ss a');
63+
64+
cy.get('kbd')
65+
.trigger('mouseover')
66+
.then(() => {
67+
cy.get('.MuiTooltip-tooltip').should('contain', expectedTooltipTimestamp);
68+
});
69+
70+
cy.contains('approved this contribution').should('not.exist');
71+
});
72+
});

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@finos/git-proxy",
3-
"version": "1.9.3",
3+
"version": "1.10.0",
44
"description": "Deploy custom push protections and policies on top of Git.",
55
"scripts": {
66
"cli": "node ./packages/git-proxy-cli/index.js",

src/proxy/actions/Action.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ class Action {
3636
authorised: boolean = false;
3737
canceled: boolean = false;
3838
rejected: boolean = false;
39+
autoApproved: boolean = false;
40+
autoRejected: boolean = false;
3941
commitData?: Commit[] = [];
4042
commitFrom?: string;
4143
commitTo?: string;
@@ -128,6 +130,20 @@ class Action {
128130
this.blocked = false;
129131
}
130132

133+
/**
134+
* Set auto approval for the action.
135+
*/
136+
setAutoApproval(): void {
137+
this.autoApproved = true;
138+
}
139+
140+
/**
141+
* Set auto rejection for the action.
142+
*/
143+
setAutoRejection(): void {
144+
this.autoRejected = true;
145+
}
146+
131147
/**
132148
* Check if the action can continue.
133149
* @return {boolean} true if the action can continue, false otherwise

src/proxy/actions/autoActions.js

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
const db = require('../../db');
2+
3+
const attemptAutoApproval = async (action) => {
4+
try {
5+
const attestation = {
6+
timestamp: new Date(),
7+
autoApproved: true,
8+
};
9+
await db.authorise(action.id, attestation);
10+
console.log('Push automatically approved by system.');
11+
12+
return true;
13+
} catch (error) {
14+
console.error('Error during auto-approval:', error.message);
15+
return false;
16+
}
17+
};
18+
19+
const attemptAutoRejection = async (action) => {
20+
try {
21+
const attestation = {
22+
timestamp: new Date(),
23+
autoApproved: true,
24+
};
25+
await db.reject(action.id, attestation);
26+
console.log('Push automatically rejected by system.');
27+
28+
return true;
29+
} catch (error) {
30+
console.error('Error during auto-rejection:', error.message);
31+
return false;
32+
}
33+
};
34+
35+
module.exports = {
36+
attemptAutoApproval,
37+
attemptAutoRejection,
38+
};

src/proxy/chain.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { PluginLoader } from '../plugin';
22
import { Action } from './actions';
33
import * as proc from './processors';
4+
import { attemptAutoApproval, attemptAutoRejection } from './actions/autoActions';
45

56
const pushActionChain: ((req: any, action: Action) => Promise<Action>)[] = [
67
proc.push.parsePush,
@@ -23,7 +24,7 @@ const pullActionChain: ((req: any, action: Action) => Promise<Action>)[] = [proc
2324
let pluginsInserted = false;
2425

2526
export const executeChain = async (req: any, res: any): Promise<Action> => {
26-
let action: Action;
27+
let action: Action = {} as Action;
2728
try {
2829
action = await proc.pre.parseAction(req);
2930
const actionFns = await getChain(action);
@@ -39,7 +40,12 @@ export const executeChain = async (req: any, res: any): Promise<Action> => {
3940
}
4041
}
4142
} finally {
42-
await proc.push.audit(req, action!);
43+
await proc.push.audit(req, action);
44+
if (action.autoApproved) {
45+
attemptAutoApproval(action);
46+
} else if (action.autoRejected) {
47+
attemptAutoRejection(action);
48+
}
4349
}
4450

4551
return action;

src/proxy/processors/push-action/preReceive.ts

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ const exec = async (
1313
hookFilePath: string = './hooks/pre-receive.sh'
1414
): Promise<Action> => {
1515
const step = new Step('executeExternalPreReceiveHook');
16+
let stderrTrimmed = '';
1617

1718
try {
1819
const resolvedPath = path.resolve(hookFilePath);
@@ -38,23 +39,33 @@ const exec = async (
3839

3940
const { stdout, stderr, status } = hookProcess;
4041

41-
const stderrTrimmed = stderr ? stderr.trim() : '';
42+
stderrTrimmed = stderr ? stderr.trim() : '';
4243
const stdoutTrimmed = stdout ? stdout.trim() : '';
4344

44-
if (status !== 0) {
45+
step.log(`Hook exited with status ${status}`);
46+
47+
if (status === 0) {
48+
step.log('Push automatically approved by pre-receive hook.');
49+
action.addStep(step);
50+
action.setAutoApproval();
51+
} else if (status === 1) {
52+
step.log('Push automatically rejected by pre-receive hook.');
53+
action.addStep(step);
54+
action.setAutoRejection();
55+
} else if (status === 2) {
56+
step.log('Push requires manual approval.');
57+
action.addStep(step);
58+
} else {
4559
step.error = true;
46-
step.log(`Hook stderr: ${stderrTrimmed}`);
47-
step.setError(stdoutTrimmed);
60+
step.log(`Unexpected hook status: ${status}`);
61+
step.setError(stdoutTrimmed || 'Unknown pre-receive hook error.');
4862
action.addStep(step);
49-
return action;
5063
}
51-
52-
step.log('Pre-receive hook executed successfully');
53-
action.addStep(step);
5464
return action;
5565
} catch (error: any) {
5666
step.error = true;
57-
step.setError(`Hook execution error: ${error.message}`);
67+
step.log('Push failed, pre-receive hook returned an error.');
68+
step.setError(`Hook execution error: ${stderrTrimmed || error.message}`);
5869
action.addStep(step);
5970
return action;
6071
}

src/ui/views/PushDetails/PushDetails.jsx

Lines changed: 57 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -173,45 +173,67 @@ export default function Dashboard() {
173173
}}
174174
>
175175
<CheckCircle
176-
style={{ cursor: 'pointer', transform: 'scale(0.65)' }}
177-
onClick={() => setAttestation(true)}
176+
style={{
177+
cursor: data.autoApproved ? 'default' : 'pointer',
178+
transform: 'scale(0.65)',
179+
opacity: data.autoApproved ? 0.5 : 1,
180+
}}
181+
onClick={() => {
182+
if (!data.autoApproved) {
183+
setAttestation(true);
184+
}
185+
}}
178186
htmlColor='green'
179187
/>
180188
</span>
181-
<a href={`/admin/user/${data.attestation.reviewer.username}`}>
182-
<img
183-
style={{ width: '45px', borderRadius: '20px' }}
184-
src={`https://github.com/${data.attestation.reviewer.gitAccount}.png`}
185-
/>
186-
</a>
187-
<div>
188-
<p>
189+
190+
{data.autoApproved ? (
191+
<>
192+
<div style={{ paddingTop: '15px' }}>
193+
<p>
194+
<strong>Auto-approved by system</strong>
195+
</p>
196+
</div>
197+
</>
198+
) : (
199+
<>
189200
<a href={`/admin/user/${data.attestation.reviewer.username}`}>
190-
{data.attestation.reviewer.gitAccount}
191-
</a>{' '}
192-
approved this contribution
193-
</p>
194-
<Tooltip
195-
title={moment(data.attestation.timestamp).format(
196-
'dddd, MMMM Do YYYY, h:mm:ss a',
197-
)}
198-
arrow
199-
>
200-
<kbd
201-
style={{
202-
color: 'black',
203-
float: 'right',
204-
}}
205-
>
206-
{moment(data.attestation.timestamp).fromNow()}
207-
</kbd>
208-
</Tooltip>
209-
</div>
210-
<AttestationView
211-
data={data.attestation}
212-
attestation={attestation}
213-
setAttestation={setAttestation}
214-
/>
201+
<img
202+
style={{ width: '45px', borderRadius: '20px' }}
203+
src={`https://github.com/${data.attestation.reviewer.gitAccount}.png`}
204+
/>
205+
</a>
206+
<div>
207+
<p>
208+
<a href={`/admin/user/${data.attestation.reviewer.username}`}>
209+
{data.attestation.reviewer.gitAccount}
210+
</a>{' '}
211+
approved this contribution
212+
</p>
213+
</div>
214+
</>
215+
)}
216+
217+
<Tooltip
218+
title={moment(data.attestation.timestamp).format(
219+
'dddd, MMMM Do YYYY, h:mm:ss a',
220+
)}
221+
arrow
222+
>
223+
<kbd style={{ color: 'black', float: 'right' }}>
224+
{moment(data.attestation.timestamp).fromNow()}
225+
</kbd>
226+
</Tooltip>
227+
228+
{data.autoApproved ? (
229+
<></>
230+
) : (
231+
<AttestationView
232+
data={data.attestation}
233+
attestation={attestation}
234+
setAttestation={setAttestation}
235+
/>
236+
)}
215237
</div>
216238
) : null}
217239
</CardHeader>

0 commit comments

Comments
 (0)