Skip to content
This repository was archived by the owner on Jun 15, 2026. It is now read-only.

Commit 46945c8

Browse files
committed
Merge branch 'main' of github.com:RMCampos/tasknote
2 parents a04a01c + 3b956c5 commit 46945c8

30 files changed

Lines changed: 318 additions & 62 deletions

File tree

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
name: Deploy to prod
1+
name: Main CD-Deploy to Prod
22

33
on:
44
workflow_dispatch:
@@ -14,7 +14,7 @@ on:
1414
required: false
1515
default: "true"
1616
workflow_run:
17-
workflows: [ "Backend Main", "Frontend Main" ]
17+
workflows: [ "Main CI-Backend", "Main CI-Frontend" ]
1818
types: [ completed ]
1919

2020
jobs:
@@ -135,6 +135,7 @@ jobs:
135135
&& needs.terraform-plan.outputs.no_changes == 'false'
136136
environment:
137137
name: production
138+
url: https://tasknote.darkroasted.vps-kinghost.net
138139
permissions:
139140
contents: read
140141
steps:
Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
1-
name: Deploy to staging
1+
name: Pull Request CD-Deploy to Staging
22

33
on:
44
workflow_dispatch:
55
workflow_run:
6-
workflows: [ "Backend PR", "Frontend PR" ]
6+
workflows: [ "Pull Request CI-Backend", "Pull Request CI-Frontend" ]
77
types: [ completed ]
88

99
jobs:
1010
terraform-plan-stg:
1111
name: Plan changs to staging
12+
if: ${{ github.event.workflow_run.conclusion == 'success' }}
1213
runs-on: ubuntu-latest
1314
outputs:
1415
no_changes: ${{ steps.check-changes.outputs.no_changes }}
@@ -99,6 +100,7 @@ jobs:
99100
if: needs.terraform-plan-stg.outputs.no_changes == 'false'
100101
environment:
101102
name: staging
103+
url: https://tasknote-stg.darkroasted.vps-kinghost.net
102104
permissions:
103105
contents: read
104106
steps:
Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
name: Backend Main
1+
name: Main CI-Backend
22

33
on:
44
workflow_dispatch:
@@ -69,18 +69,29 @@ jobs:
6969
username: ${{ github.actor }}
7070
password: ${{ secrets.GITHUB_TOKEN }}
7171

72-
- name: Build Docker image with Spring Boot
73-
working-directory: ./server
72+
- name: Set up Docker Buildx
73+
uses: docker/setup-buildx-action@v3
74+
75+
- name: Find PR number
76+
id: find_pr
77+
env:
78+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
7479
run: |
75-
./mvnw -Pnative -DskipTests spring-boot:build-image \
76-
-Dspring-boot.build-image.imageName=ghcr.io/${{ steps.repo.outputs.name }}/api:latest \
77-
-Dspring-boot.build-image.builder=paketobuildpacks/builder-jammy-tiny:latest
80+
PR_NUMBER=$(gh pr list --search "${{ github.sha }}" --state merged --json number --jq '.[0].number')
81+
if [ -z "$PR_NUMBER" ]; then
82+
echo "No merged PR found for this commit. Falling back to 'candidate' tag."
83+
PR_NUMBER="candidate"
84+
else
85+
PR_NUMBER="pr-${PR_NUMBER}"
86+
fi
87+
echo "tag=${PR_NUMBER}" >> $GITHUB_OUTPUT
7888
79-
- name: Tag and push Docker image
89+
- name: Promote Docker image
8090
run: |
81-
docker tag ghcr.io/${{ steps.repo.outputs.name }}/api:latest ghcr.io/${{ steps.repo.outputs.name }}/api:${{ steps.version.outputs.version }}
82-
docker push ghcr.io/${{ steps.repo.outputs.name }}/api:latest
83-
docker push ghcr.io/${{ steps.repo.outputs.name }}/api:${{ steps.version.outputs.version }}
91+
docker buildx imagetools create \
92+
--tag ghcr.io/${{ steps.repo.outputs.name }}/api:latest \
93+
--tag ghcr.io/${{ steps.repo.outputs.name }}/api:${{ steps.version.outputs.version }} \
94+
ghcr.io/${{ steps.repo.outputs.name }}/api:${{ steps.find_pr.outputs.tag }}
8495
8596
- name: Create and push Git tag
8697
run: |
Lines changed: 24 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
name: Frontend Main
1+
name: Main CI-Frontend
22

33
on:
44
workflow_dispatch:
@@ -31,6 +31,10 @@ jobs:
3131
with:
3232
fetch-depth: 0
3333

34+
- name: Set lowercase repo name
35+
id: repo
36+
run: echo "name=${GITHUB_REPOSITORY,,}" >> $GITHUB_OUTPUT
37+
3438
- name: Generate version tag
3539
id: version
3640
run: |
@@ -49,26 +53,26 @@ jobs:
4953
username: ${{ github.actor }}
5054
password: ${{ secrets.GITHUB_TOKEN }}
5155

52-
- name: Extract metadata for Docker
53-
id: meta
54-
uses: docker/metadata-action@v5
55-
with:
56-
images: ghcr.io/${{ github.repository }}/app
57-
tags: |
58-
type=raw,value=${{ steps.version.outputs.tag }}
59-
type=raw,value=latest,enable={{is_default_branch}}
56+
- name: Find PR number
57+
id: find_pr
58+
env:
59+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
60+
run: |
61+
PR_NUMBER=$(gh pr list --search "${{ github.sha }}" --state merged --json number --jq '.[0].number')
62+
if [ -z "$PR_NUMBER" ]; then
63+
echo "No merged PR found for this commit. Falling back to 'candidate' tag."
64+
PR_NUMBER="candidate"
65+
else
66+
PR_NUMBER="pr-${PR_NUMBER}"
67+
fi
68+
echo "tag=${PR_NUMBER}" >> $GITHUB_OUTPUT
6069
61-
- name: Build and push Docker image
62-
uses: docker/build-push-action@v5
63-
with:
64-
context: ./client
65-
push: true
66-
tags: ${{ steps.meta.outputs.tags }}
67-
labels: ${{ steps.meta.outputs.labels }}
68-
cache-from: type=gha
69-
cache-to: type=gha,mode=max
70-
build-args: |
71-
VITE_BUILD=v${{ steps.version.outputs.tag }}
70+
- name: Promote Docker image
71+
run: |
72+
docker buildx imagetools create \
73+
--tag ghcr.io/${{ steps.repo.outputs.name }}/app:latest \
74+
--tag ghcr.io/${{ steps.repo.outputs.name }}/app:${{ steps.version.outputs.tag }} \
75+
ghcr.io/${{ steps.repo.outputs.name }}/app:${{ steps.find_pr.outputs.tag }}
7276
7377
- name: Create and push Git tag
7478
run: |
Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
name: Backend PR
1+
name: Pull Request CI-Backend
22

33
on:
44
workflow_dispatch:
@@ -51,6 +51,7 @@ jobs:
5151
needs: ["run-checks"]
5252
permissions:
5353
contents: read
54+
deployments: write
5455
packages: write
5556

5657
steps:
@@ -91,10 +92,38 @@ jobs:
9192
working-directory: ./server
9293
run: |
9394
./mvnw -Pnative -DskipTests spring-boot:build-image \
94-
-Dspring-boot.build-image.imageName=ghcr.io/rmcampos/tasknote/api:latest \
95+
-Dspring-boot.build-image.imageName=ghcr.io/${{ steps.repo.outputs.name }}/api:latest \
9596
-Dspring-boot.build-image.builder=paketobuildpacks/builder-jammy-tiny:latest
9697
9798
- name: Tag and push Docker image
9899
run: |
99100
docker tag ghcr.io/${{ steps.repo.outputs.name }}/api:latest ghcr.io/${{ steps.repo.outputs.name }}/api:candidate
101+
docker tag ghcr.io/${{ steps.repo.outputs.name }}/api:latest ghcr.io/${{ steps.repo.outputs.name }}/api:pr-${{ github.event.pull_request.number }}
100102
docker push ghcr.io/${{ steps.repo.outputs.name }}/api:candidate
103+
docker push ghcr.io/${{ steps.repo.outputs.name }}/api:pr-${{ github.event.pull_request.number }}
104+
105+
- name: Create GitHub deployment for staging
106+
if: ${{ github.event_name == 'pull_request' }}
107+
uses: actions/github-script@v6
108+
with:
109+
script: |
110+
const ref = context.payload.pull_request.head.sha;
111+
const env = 'staging';
112+
const resp = await github.rest.repos.createDeployment({
113+
owner: context.repo.owner,
114+
repo: context.repo.repo,
115+
ref,
116+
required_contexts: [],
117+
environment: env,
118+
description: `PR #${context.payload.pull_request.number} preview deployment`,
119+
transient_environment: true,
120+
auto_merge: false
121+
});
122+
// create a deployment status pointing to the staging URL
123+
await github.rest.repos.createDeploymentStatus({
124+
owner: context.repo.owner,
125+
repo: context.repo.repo,
126+
deployment_id: resp.data.id,
127+
state: 'success',
128+
environment_url: 'https://tasknote-stg.darkroasted.vps-kinghost.net'
129+
});
Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
name: Frontend PR
1+
name: Pull Request CI-Frontend
22

33
on:
44
workflow_dispatch:
@@ -60,6 +60,7 @@ jobs:
6060
permissions:
6161
contents: write
6262
packages: write
63+
deployments: write
6364

6465
steps:
6566
- name: Checkout code
@@ -84,6 +85,7 @@ jobs:
8485
images: ghcr.io/${{ github.repository }}/app
8586
tags: |
8687
type=raw,value=candidate
88+
type=raw,value=pr-${{ github.event.pull_request.number }}
8789
8890
- name: Generate version tag
8991
id: version
@@ -104,3 +106,29 @@ jobs:
104106
cache-to: type=gha,mode=max
105107
build-args: |
106108
VITE_BUILD=${{ steps.version.outputs.tag }}
109+
110+
- name: Create GitHub deployment for staging
111+
if: ${{ github.event_name == 'pull_request' }}
112+
uses: actions/github-script@v6
113+
with:
114+
script: |
115+
const ref = context.payload.pull_request.head.sha;
116+
const env = 'staging';
117+
const resp = await github.rest.repos.createDeployment({
118+
owner: context.repo.owner,
119+
repo: context.repo.repo,
120+
ref,
121+
required_contexts: [],
122+
environment: env,
123+
description: `PR #${context.payload.pull_request.number} preview deployment`,
124+
transient_environment: true,
125+
auto_merge: false
126+
});
127+
// create a deployment status pointing to the staging URL
128+
await github.rest.repos.createDeploymentStatus({
129+
owner: context.repo.owner,
130+
repo: context.repo.repo,
131+
deployment_id: resp.data.id,
132+
state: 'success',
133+
environment_url: 'https://tasknote-stg.darkroasted.vps-kinghost.net'
134+
});
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { describe, it, expect } from 'vitest';
2+
import { isSafeUrl } from '../../utils/UrlUtils';
3+
4+
describe('UrlUtils', () => {
5+
it('should allow http:// URLs', () => {
6+
expect(isSafeUrl('http://example.com')).toBe(true);
7+
});
8+
9+
it('should allow https:// URLs', () => {
10+
expect(isSafeUrl('https://example.com')).toBe(true);
11+
});
12+
13+
it('should allow # URLs', () => {
14+
expect(isSafeUrl('#section')).toBe(true);
15+
});
16+
17+
it('should disallow javascript: URLs', () => {
18+
expect(isSafeUrl('javascript:alert(1)')).toBe(false);
19+
});
20+
21+
it('should disallow data: URLs', () => {
22+
expect(isSafeUrl('data:text/html,<script>alert(1)</script>')).toBe(false);
23+
});
24+
25+
it('should disallow empty or null URLs', () => {
26+
expect(isSafeUrl('')).toBe(false);
27+
expect(isSafeUrl(null)).toBe(false);
28+
expect(isSafeUrl(undefined)).toBe(false);
29+
});
30+
31+
it('should be case insensitive for protocol', () => {
32+
expect(isSafeUrl('HTTP://example.com')).toBe(true);
33+
expect(isSafeUrl('HTTPS://example.com')).toBe(true);
34+
});
35+
});

client/src/api-service/apiConfig.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { env } from '../env';
22

3-
const server = env.VITE_BACKEND_SERVER;
3+
const server = env.VITE_BACKEND_SERVER || '/api';
44

55
const ApiConfig = {
66

client/src/components/NoteTitle/index.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import React from 'react';
22
import ExternalLinkIcon from '../../assets/icons8-external-link-30.png';
3+
import { isSafeUrl } from '../../utils/UrlUtils';
34

45
interface Props {
56
readonly title: string;
@@ -18,8 +19,8 @@ function NoteTitle(props: React.PropsWithChildren<Props>): React.ReactNode {
1819
<span className="task-title-icon">
1920
<span className="poppins-semibold">
2021
{props.title}
21-
{props.noteUrl && props.noteUrl.length > 0 && (
22-
<a href={props.noteUrl} target="_blank" rel="noreferrer" className="task-note-external-link">
22+
{isSafeUrl(props.noteUrl) && (
23+
<a href={props.noteUrl!} target="_blank" rel="noreferrer" className="task-note-external-link">
2324
<img src={ExternalLinkIcon} width={20} alt="external link" />
2425
</a>
2526
)}

client/src/components/TaskTitle/index.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import React from 'react';
22
import ExternalLinkIcon from '../../assets/icons8-external-link-30.png';
3+
import { isSafeUrl } from '../../utils/UrlUtils';
34
import './style.css';
45

56
interface Props {
@@ -24,7 +25,7 @@ function TaskTitle(props: React.PropsWithChildren<Props>): React.ReactNode {
2425
data-testid={`task-title-text-${props.title}`}
2526
>
2627
{props.title}
27-
{props.taskUrl && props.taskUrl.length > 0 && (
28+
{props.taskUrl && props.taskUrl.length > 0 && isSafeUrl(props.taskUrl[0]) && (
2829
<a href={props.taskUrl[0]} target="_blank" rel="noreferrer" className="task-note-external-link">
2930
<img src={ExternalLinkIcon} width={20} alt="external link" />
3031
</a>

0 commit comments

Comments
 (0)