Skip to content

Commit 7fe2e29

Browse files
authored
Merge pull request #1139 from fabiovincenzi/push-tags
feat: Add Annotated Tag Push Support
2 parents f0871ea + 1bab1cc commit 7fe2e29

27 files changed

Lines changed: 2211 additions & 301 deletions

File tree

.eslintrc.json

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
{
2+
"parser": "@typescript-eslint/parser",
3+
"env": {
4+
"node": true,
5+
"browser": true,
6+
"commonjs": true,
7+
"es2021": true,
8+
"mocha": true
9+
},
10+
"extends": [
11+
"eslint:recommended",
12+
"plugin:@typescript-eslint/recommended",
13+
"plugin:react/recommended",
14+
"google",
15+
"prettier",
16+
"plugin:json/recommended"
17+
],
18+
"overrides": [
19+
{
20+
"files": ["test/**/*.js", "**/*.json", "cypress/**/*.js", "plugins/**/*.js"],
21+
"parserOptions": {
22+
"project": null
23+
},
24+
"parser": "espree",
25+
"env": {
26+
"cypress/globals": true
27+
},
28+
"plugins": ["cypress"],
29+
"rules": {
30+
"@typescript-eslint/no-unused-expressions": "off"
31+
}
32+
}
33+
],
34+
"parserOptions": {
35+
"project": "./tsconfig.json",
36+
"requireConfigFile": false,
37+
"ecmaVersion": 12,
38+
"sourceType": "module",
39+
"ecmaFeatures": {
40+
"jsx": true,
41+
"modules": true
42+
},
43+
"babelOptions": {
44+
"presets": ["@babel/preset-react"]
45+
}
46+
},
47+
"plugins": ["@typescript-eslint", "react", "prettier"],
48+
"rules": {
49+
"react/prop-types": "off",
50+
"require-jsdoc": "off",
51+
"no-async-promise-executor": "off",
52+
"@typescript-eslint/no-explicit-any": "off",
53+
"@typescript-eslint/no-unused-vars": "off",
54+
"@typescript-eslint/no-require-imports": "off",
55+
"@typescript-eslint/no-unused-expressions": "off"
56+
},
57+
"settings": {
58+
"react": {
59+
"version": "detect"
60+
}
61+
},
62+
"ignorePatterns": ["src/config/generated/config.ts"]
63+
}

cypress/e2e/tagPush.cy.js

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
/**
2+
* Copyright 2026 GitProxy Contributors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
describe('Tag Push Functionality', () => {
18+
beforeEach(() => {
19+
cy.login('admin', 'admin');
20+
cy.on('uncaught:exception', () => false);
21+
22+
// Create test data for tag pushes
23+
cy.createTestTagPush();
24+
});
25+
26+
describe('Tag Push Display in PushesTable', () => {
27+
it('can navigate to push dashboard and view push table', () => {
28+
cy.visit('/dashboard/push');
29+
30+
// Wait for API call to complete
31+
cy.wait('@getPushes');
32+
33+
// Check that we can see the basic table structure
34+
cy.get('table', { timeout: 10000 }).should('exist');
35+
cy.get('thead').should('exist');
36+
cy.get('tbody').should('exist');
37+
38+
// Now we should have test data, so we can check for rows
39+
cy.get('tbody tr').should('have.length.at.least', 1);
40+
41+
// Check the structure of the first row
42+
cy.get('tbody tr')
43+
.first()
44+
.within(() => {
45+
cy.get('td').should('have.length.at.least', 6); // We know there are multiple columns
46+
// Check for tag-specific content
47+
cy.contains('v1.0.0').should('exist'); // Tag name
48+
cy.contains('test-tagger').should('exist'); // Tagger
49+
});
50+
});
51+
52+
it('can interact with push table entries', () => {
53+
cy.visit('/dashboard/push');
54+
cy.wait('@getPushes');
55+
56+
cy.get('tbody tr').should('have.length.at.least', 1);
57+
58+
// Check for clickable elements in the first row
59+
cy.get('tbody tr')
60+
.first()
61+
.within(() => {
62+
// Should have links and buttons
63+
cy.get('a').should('have.length.at.least', 1); // Repository links, etc.
64+
cy.get('button').should('have.length.at.least', 1); // Action button
65+
});
66+
});
67+
});
68+
69+
describe('Tag Push Details Page', () => {
70+
it('can access push details page structure', () => {
71+
// Try to access a push details page directly
72+
cy.visit('/dashboard/push/test-push-id', { failOnStatusCode: false });
73+
74+
// Check basic page structure exists (regardless of whether push exists)
75+
cy.get('body').should('exist'); // Basic content check
76+
77+
// If we end up redirected, that's also acceptable behavior
78+
cy.url().should('include', '/dashboard');
79+
});
80+
});
81+
82+
describe('Basic UI Navigation', () => {
83+
it('can navigate between dashboard pages', () => {
84+
cy.visit('/dashboard/push');
85+
cy.wait('@getPushes');
86+
cy.get('table', { timeout: 10000 }).should('exist');
87+
88+
// Test navigation to repo dashboard
89+
cy.visit('/dashboard/repo');
90+
cy.get('table', { timeout: 10000 }).should('exist');
91+
92+
// Test navigation to user management if it exists
93+
cy.visit('/dashboard/user');
94+
cy.get('body').should('exist');
95+
});
96+
});
97+
98+
describe('Application Robustness', () => {
99+
it('handles navigation to non-existent push gracefully', () => {
100+
// Try to visit a non-existent push detail page
101+
cy.visit('/dashboard/push/non-existent-push-id', { failOnStatusCode: false });
102+
103+
// Should either redirect or show error page, but not crash
104+
cy.get('body').should('exist');
105+
});
106+
107+
it('maintains functionality after page refresh', () => {
108+
cy.visit('/dashboard/push');
109+
cy.wait('@getPushes');
110+
cy.get('table', { timeout: 10000 }).should('exist');
111+
112+
// Refresh the page
113+
cy.reload();
114+
// Wait for API call again after reload
115+
cy.wait('@getPushes');
116+
117+
// Wait for page to reload and check basic functionality
118+
cy.get('body').should('exist');
119+
120+
// Give more time for table to load after refresh, or check if redirected
121+
cy.url().then((url) => {
122+
if (url.includes('/dashboard/push')) {
123+
cy.get('table', { timeout: 15000 }).should('exist');
124+
} else {
125+
// If redirected (e.g., to login), that's also acceptable behavior
126+
cy.get('body').should('exist');
127+
}
128+
});
129+
});
130+
});
131+
});

cypress/support/commands.js

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,69 @@ Cypress.Commands.add('getCSRFToken', () => {
9494
});
9595
});
9696

97+
Cypress.Commands.add('createTestTagPush', (pushData = {}) => {
98+
const defaultTagPush = {
99+
id: `test-tag-push-${Date.now()}`,
100+
steps: [],
101+
error: false,
102+
blocked: true,
103+
allowPush: false,
104+
authorised: false,
105+
canceled: false,
106+
rejected: false,
107+
autoApproved: false,
108+
autoRejected: false,
109+
type: 'push',
110+
method: 'get',
111+
timestamp: Date.now(),
112+
project: 'cypress-test',
113+
repoName: 'test-repo.git',
114+
url: 'https://github.com/cypress-test/test-repo.git',
115+
repo: 'cypress-test/test-repo.git',
116+
user: 'test-tagger',
117+
userEmail: 'test-tagger@test.com',
118+
branch: 'refs/heads/main',
119+
tags: ['refs/tags/v1.0.0'],
120+
commitFrom: '0000000000000000000000000000000000000000',
121+
commitTo: 'abcdef1234567890abcdef1234567890abcdef12',
122+
lastStep: null,
123+
blockedMessage: '\n\n\nGitProxy has received your tag push\n\n\n',
124+
_id: null,
125+
attestation: null,
126+
tagData: [
127+
{
128+
tagName: 'v1.0.0',
129+
type: 'annotated',
130+
tagger: 'test-tagger',
131+
message: 'Release version 1.0.0\n\nThis is a test tag release for Cypress testing.',
132+
timestamp: Math.floor(Date.now() / 1000),
133+
},
134+
],
135+
commitData: [
136+
{
137+
commitTs: Math.floor(Date.now() / 1000) - 300,
138+
commitTimestamp: Math.floor(Date.now() / 1000) - 300,
139+
message: 'feat: add new tag push feature',
140+
committer: 'test-committer',
141+
author: 'test-author',
142+
authorEmail: 'test-author@test.com',
143+
},
144+
],
145+
diff: {
146+
content: '+++ test tag push implementation',
147+
},
148+
...pushData,
149+
};
150+
151+
// For now, intercept the push API calls and return our test data
152+
cy.intercept('GET', '**/api/v1/push*', {
153+
statusCode: 200,
154+
body: [defaultTagPush],
155+
}).as('getPushes');
156+
157+
return cy.wrap(defaultTagPush);
158+
});
159+
97160
Cypress.Commands.add('createUser', (username, password, email, gitAccount) => {
98161
cy.request({
99162
method: 'POST',

src/db/mongo/pushes.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,9 +52,12 @@ export const getPushes = async (
5252
rejected: 1,
5353
repo: 1,
5454
repoName: 1,
55+
tag: 1,
56+
tagData: 1,
5557
timestamp: 1,
5658
type: 1,
5759
url: 1,
60+
user: 1,
5861
},
5962
sort: { timestamp: -1 },
6063
});

src/db/types.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,31 @@ export class User {
108108
}
109109
}
110110

111+
export type Push = {
112+
id: string;
113+
allowPush: boolean;
114+
authorised: boolean;
115+
blocked: boolean;
116+
blockedMessage: string;
117+
branch: string;
118+
canceled: boolean;
119+
commitData: object;
120+
commitFrom: string;
121+
commitTo: string;
122+
error: boolean;
123+
method: string;
124+
project: string;
125+
rejected: boolean;
126+
repo: string;
127+
repoName: string;
128+
tag?: string;
129+
tagData?: object;
130+
timepstamp: string;
131+
type: string;
132+
url: string;
133+
user?: string;
134+
};
135+
111136
export interface PublicUser {
112137
username: string;
113138
displayName: string;

src/proxy/actions/Action.ts

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,31 @@
1717
import { processGitURLForNameAndOrg, processUrlPath } from '../routes/helper';
1818
import { Step } from './Step';
1919
import { CompletedAttestation, CommitData, Rejection } from '../processors/types';
20+
import { TagData } from '../../types/models';
21+
22+
export enum RequestType {
23+
PUSH = 'push',
24+
25+
PULL = 'pull',
26+
27+
DEFAULT = 'default',
28+
}
29+
30+
export enum PushType {
31+
/** Push to a tag ref (refs/tags/*) */
32+
TAG = 'tag',
33+
34+
/** Push to a branch ref (refs/heads/*) or any other non-tag ref */
35+
BRANCH = 'branch',
36+
}
2037

2138
/**
2239
* Class representing a Push.
2340
*/
2441
class Action {
2542
id: string;
26-
type: string;
43+
type: RequestType;
44+
actionType?: PushType;
2745
method: string;
2846
timestamp: number;
2947
project: string;
@@ -53,6 +71,8 @@ class Action {
5371
rejection?: Rejection;
5472
lastStep?: Step;
5573
proxyGitPath?: string;
74+
tags?: string[];
75+
tagData?: TagData[];
5676
newIdxFiles?: string[];
5777
protocol?: 'https' | 'ssh';
5878
pullAuthStrategy?:
@@ -70,7 +90,7 @@ class Action {
7090
* @param {number} timestamp The timestamp of the action
7191
* @param {string} url The URL to the repo that should be proxied (with protocol, origin, repo path, but not the path for the git operation).
7292
*/
73-
constructor(id: string, type: string, method: string, timestamp: number, url: string) {
93+
constructor(id: string, type: RequestType, method: string, timestamp: number, url: string) {
7494
this.id = id;
7595
this.type = type;
7696
this.method = method;

src/proxy/actions/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
* limitations under the License.
1515
*/
1616

17-
import { Action } from './Action';
17+
import { Action, RequestType, PushType } from './Action';
1818
import { Step } from './Step';
1919

20-
export { Action, Step };
20+
export { Action, Step, RequestType, PushType };

0 commit comments

Comments
 (0)