Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
adc1362
chore: fix eslintrc.json — remove invalid JS comments
fabiovincenzi Mar 11, 2026
62896c9
feat: add ActionType and RequestType enums
fabiovincenzi Mar 11, 2026
b711fb5
feat: add tag push parsing (parseTag, parsePush, constants)
fabiovincenzi Mar 11, 2026
83a3550
feat: add tag push chain and audit processor
fabiovincenzi Mar 11, 2026
5370825
feat: add TagData type and DB support
fabiovincenzi Mar 11, 2026
c84142c
feat: add tag push UI support
fabiovincenzi Mar 11, 2026
ddfae87
test: add unit and integration tests for tag push
fabiovincenzi Mar 11, 2026
8675ef3
test: add cypress e2e tests for tag push
fabiovincenzi Mar 11, 2026
8dec82c
test: add cypress e2e tests for tag push
fabiovincenzi Mar 11, 2026
514b5d3
Merge remote-tracking branch 'upstream/main' into push-tags
fabiovincenzi Apr 29, 2026
d5cc199
Merge branch 'main' into push-tags
fabiovincenzi May 4, 2026
3271f39
Merge branch 'main' into push-tags
fabiovincenzi May 18, 2026
2f71407
Merge branch 'main' into push-tags
fabiovincenzi Jun 1, 2026
37598a2
Merge branch 'main' into push-tags
kriswest Jun 5, 2026
7b37e81
feat: add DEFAULT value to RequestType enum
fabiovincenzi Jun 10, 2026
87b12a9
refactor: merge ActionType.COMMIT into BRANCH
fabiovincenzi Jun 10, 2026
ed018ba
refactor: add REFS_PREFIX const
fabiovincenzi Jun 10, 2026
d36feb1
feat: support multi-tag push
fabiovincenzi Jun 10, 2026
f3845ad
refactor: removing intermediate ParsedObjects in parsePush
fabiovincenzi Jun 10, 2026
fb92bb9
fix: update log message to mention both commit and tag data
fabiovincenzi Jun 11, 2026
91ed17f
refactor: use action.actionType instead of array length checks in par…
fabiovincenzi Jun 11, 2026
4045d5e
Merge branch 'push-tags' of https://github.com/fabiovincenzi/git-prox…
fabiovincenzi Jun 11, 2026
33b1d50
refactor(parsePush): structure user extraction by actionType
fabiovincenzi Jun 11, 2026
2d294a0
refactor(parsePush): rename parseTag to getTagData
fabiovincenzi Jun 11, 2026
167d9d0
feat(chain): validate tag messages with the same rules as commit mess…
fabiovincenzi Jun 11, 2026
20e86c0
refactor(chain): inline getPushChain
fabiovincenzi Jun 11, 2026
227aef1
refactor(pushUtils): use type guard for getDisplayTimestamp
fabiovincenzi Jun 11, 2026
2575225
Merge upstream/main into push-tags
fabiovincenzi Jun 11, 2026
dc2a230
feat(parsePush): block lightweight tags with error
fabiovincenzi Jun 15, 2026
276adc8
refactor(actions): rename ActionType to PushType
fabiovincenzi Jun 15, 2026
bac2ff7
Merge remote-tracking branch 'upstream/main' into push-tags
fabiovincenzi Jun 15, 2026
07fd5b3
fix(cypress): use tags array in test tag push mock data
fabiovincenzi Jun 15, 2026
9b2deb3
refactor(chain): use Processor['exec'] indexed type for chain arrays
fabiovincenzi Jun 15, 2026
b410b7a
fix(ui): fix errored tab query mismatch and add null safety for tag p…
fabiovincenzi Jun 17, 2026
7792e84
Merge branch 'main' into push-tags
fabiovincenzi Jun 17, 2026
f24ec5d
Merge branch 'main' into push-tags
fabiovincenzi Jun 17, 2026
b132529
refactor(chain): move parsePush to pre-processor
fabiovincenzi Jun 19, 2026
10df4f1
Merge branch 'push-tags' of https://github.com/fabiovincenzi/git-prox…
fabiovincenzi Jun 19, 2026
cf14102
Merge branch 'main' into push-tags
fabiovincenzi Jun 19, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 63 additions & 0 deletions .eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
{
"parser": "@typescript-eslint/parser",
"env": {
"node": true,
"browser": true,
"commonjs": true,
"es2021": true,
"mocha": true
},
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:react/recommended",
"google",
"prettier",
"plugin:json/recommended"
],
"overrides": [
{
"files": ["test/**/*.js", "**/*.json", "cypress/**/*.js", "plugins/**/*.js"],
"parserOptions": {
"project": null
},
"parser": "espree",
"env": {
"cypress/globals": true
},
"plugins": ["cypress"],
"rules": {
"@typescript-eslint/no-unused-expressions": "off"
}
}
],
"parserOptions": {
"project": "./tsconfig.json",
"requireConfigFile": false,
"ecmaVersion": 12,
"sourceType": "module",
"ecmaFeatures": {
"jsx": true,
"modules": true
},
"babelOptions": {
"presets": ["@babel/preset-react"]
}
},
"plugins": ["@typescript-eslint", "react", "prettier"],
"rules": {
"react/prop-types": "off",
"require-jsdoc": "off",
"no-async-promise-executor": "off",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-unused-vars": "off",
"@typescript-eslint/no-require-imports": "off",
"@typescript-eslint/no-unused-expressions": "off"
},
"settings": {
"react": {
"version": "detect"
}
},
"ignorePatterns": ["src/config/generated/config.ts"]
}
131 changes: 131 additions & 0 deletions cypress/e2e/tagPush.cy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
/**
* Copyright 2026 GitProxy Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

describe('Tag Push Functionality', () => {
beforeEach(() => {
cy.login('admin', 'admin');
cy.on('uncaught:exception', () => false);

// Create test data for tag pushes
cy.createTestTagPush();
});

describe('Tag Push Display in PushesTable', () => {
it('can navigate to push dashboard and view push table', () => {
cy.visit('/dashboard/push');

// Wait for API call to complete
cy.wait('@getPushes');

// Check that we can see the basic table structure
cy.get('table', { timeout: 10000 }).should('exist');
cy.get('thead').should('exist');
cy.get('tbody').should('exist');

// Now we should have test data, so we can check for rows
cy.get('tbody tr').should('have.length.at.least', 1);

// Check the structure of the first row
cy.get('tbody tr')
.first()
.within(() => {
cy.get('td').should('have.length.at.least', 6); // We know there are multiple columns
// Check for tag-specific content
cy.contains('v1.0.0').should('exist'); // Tag name
cy.contains('test-tagger').should('exist'); // Tagger
});
});

it('can interact with push table entries', () => {
cy.visit('/dashboard/push');
cy.wait('@getPushes');

cy.get('tbody tr').should('have.length.at.least', 1);

// Check for clickable elements in the first row
cy.get('tbody tr')
.first()
.within(() => {
// Should have links and buttons
cy.get('a').should('have.length.at.least', 1); // Repository links, etc.
cy.get('button').should('have.length.at.least', 1); // Action button
});
});
});

describe('Tag Push Details Page', () => {
it('can access push details page structure', () => {
// Try to access a push details page directly
cy.visit('/dashboard/push/test-push-id', { failOnStatusCode: false });

// Check basic page structure exists (regardless of whether push exists)
cy.get('body').should('exist'); // Basic content check

// If we end up redirected, that's also acceptable behavior
cy.url().should('include', '/dashboard');
});
});

describe('Basic UI Navigation', () => {
it('can navigate between dashboard pages', () => {
cy.visit('/dashboard/push');
cy.wait('@getPushes');
cy.get('table', { timeout: 10000 }).should('exist');

// Test navigation to repo dashboard
cy.visit('/dashboard/repo');
cy.get('table', { timeout: 10000 }).should('exist');

// Test navigation to user management if it exists
cy.visit('/dashboard/user');
cy.get('body').should('exist');
});
});

describe('Application Robustness', () => {
it('handles navigation to non-existent push gracefully', () => {
// Try to visit a non-existent push detail page
cy.visit('/dashboard/push/non-existent-push-id', { failOnStatusCode: false });

// Should either redirect or show error page, but not crash
cy.get('body').should('exist');
});

it('maintains functionality after page refresh', () => {
cy.visit('/dashboard/push');
cy.wait('@getPushes');
cy.get('table', { timeout: 10000 }).should('exist');

// Refresh the page
cy.reload();
// Wait for API call again after reload
cy.wait('@getPushes');

// Wait for page to reload and check basic functionality
cy.get('body').should('exist');

// Give more time for table to load after refresh, or check if redirected
cy.url().then((url) => {
if (url.includes('/dashboard/push')) {
cy.get('table', { timeout: 15000 }).should('exist');
} else {
// If redirected (e.g., to login), that's also acceptable behavior
cy.get('body').should('exist');
}
});
});
});
});
63 changes: 63 additions & 0 deletions cypress/support/commands.js
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,69 @@ Cypress.Commands.add('getCSRFToken', () => {
});
});

Cypress.Commands.add('createTestTagPush', (pushData = {}) => {
const defaultTagPush = {
id: `test-tag-push-${Date.now()}`,
steps: [],
error: false,
blocked: true,
allowPush: false,
authorised: false,
canceled: false,
rejected: false,
autoApproved: false,
autoRejected: false,
type: 'push',
method: 'get',
timestamp: Date.now(),
project: 'cypress-test',
repoName: 'test-repo.git',
url: 'https://github.com/cypress-test/test-repo.git',
repo: 'cypress-test/test-repo.git',
user: 'test-tagger',
userEmail: 'test-tagger@test.com',
branch: 'refs/heads/main',
tags: ['refs/tags/v1.0.0'],
commitFrom: '0000000000000000000000000000000000000000',
commitTo: 'abcdef1234567890abcdef1234567890abcdef12',
lastStep: null,
blockedMessage: '\n\n\nGitProxy has received your tag push\n\n\n',
_id: null,
attestation: null,
tagData: [
{
tagName: 'v1.0.0',
type: 'annotated',
tagger: 'test-tagger',
message: 'Release version 1.0.0\n\nThis is a test tag release for Cypress testing.',
timestamp: Math.floor(Date.now() / 1000),
},
],
commitData: [
{
commitTs: Math.floor(Date.now() / 1000) - 300,
commitTimestamp: Math.floor(Date.now() / 1000) - 300,
message: 'feat: add new tag push feature',
committer: 'test-committer',
author: 'test-author',
authorEmail: 'test-author@test.com',
},
],
diff: {
content: '+++ test tag push implementation',
},
...pushData,
};

// For now, intercept the push API calls and return our test data
cy.intercept('GET', '**/api/v1/push*', {
statusCode: 200,
body: [defaultTagPush],
}).as('getPushes');

return cy.wrap(defaultTagPush);
});

Cypress.Commands.add('createUser', (username, password, email, gitAccount) => {
cy.request({
method: 'POST',
Expand Down
3 changes: 3 additions & 0 deletions src/db/mongo/pushes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,12 @@ export const getPushes = async (
rejected: 1,
repo: 1,
repoName: 1,
tag: 1,
tagData: 1,
timestamp: 1,
type: 1,
url: 1,
user: 1,
},
sort: { timestamp: -1 },
});
Expand Down
25 changes: 25 additions & 0 deletions src/db/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,31 @@ export class User {
}
}

export type Push = {
id: string;
allowPush: boolean;
authorised: boolean;
blocked: boolean;
blockedMessage: string;
branch: string;
canceled: boolean;
commitData: object;
commitFrom: string;
commitTo: string;
error: boolean;
method: string;
project: string;
rejected: boolean;
repo: string;
repoName: string;
tag?: string;
tagData?: object;
timepstamp: string;
type: string;
url: string;
user?: string;
};

export interface PublicUser {
username: string;
displayName: string;
Expand Down
24 changes: 22 additions & 2 deletions src/proxy/actions/Action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,31 @@
import { processGitURLForNameAndOrg, processUrlPath } from '../routes/helper';
import { Step } from './Step';
import { CompletedAttestation, CommitData, Rejection } from '../processors/types';
import { TagData } from '../../types/models';

export enum RequestType {
PUSH = 'push',

PULL = 'pull',

DEFAULT = 'default',
}

export enum PushType {
/** Push to a tag ref (refs/tags/*) */
TAG = 'tag',

/** Push to a branch ref (refs/heads/*) or any other non-tag ref */
BRANCH = 'branch',
}

/**
* Class representing a Push.
*/
class Action {
id: string;
type: string;
type: RequestType;
actionType?: PushType;
method: string;
timestamp: number;
project: string;
Expand Down Expand Up @@ -53,6 +71,8 @@ class Action {
rejection?: Rejection;
lastStep?: Step;
proxyGitPath?: string;
tags?: string[];
tagData?: TagData[];
newIdxFiles?: string[];
protocol?: 'https' | 'ssh';
pullAuthStrategy?:
Expand All @@ -70,7 +90,7 @@ class Action {
* @param {number} timestamp The timestamp of the action
* @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).
*/
constructor(id: string, type: string, method: string, timestamp: number, url: string) {
constructor(id: string, type: RequestType, method: string, timestamp: number, url: string) {
this.id = id;
this.type = type;
this.method = method;
Expand Down
4 changes: 2 additions & 2 deletions src/proxy/actions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
* limitations under the License.
*/

import { Action } from './Action';
import { Action, RequestType, PushType } from './Action';
import { Step } from './Step';

export { Action, Step };
export { Action, Step, RequestType, PushType };
Loading
Loading