Skip to content

Commit

Permalink
Added CI/CD to publish to Dockerhub and Github Packages
Browse files Browse the repository at this point in the history
Signed-off-by: Akash Singh <[email protected]>
  • Loading branch information
SkySingh04 committed May 6, 2024
1 parent 042cb20 commit e4a5068
Show file tree
Hide file tree
Showing 6 changed files with 367 additions and 90 deletions.
56 changes: 56 additions & 0 deletions .github/workflows/release.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
name: Publish Docker image to dockerhub and github packages

on:
push:
branches: ['main']

jobs:
push_to_registries:
name: Push Docker image to multiple registries
runs-on: ubuntu-latest
permissions:
packages: write
contents: read
attestations: write
steps:
- name: Check out the repo
uses: actions/checkout@v4

- name: Log in to Docker Hub
uses: docker/login-action@f4ef78c080cd8ba55a85445d5b36e214a81df20a
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}

- name: Log in to the Container registry
uses: docker/login-action@65b78e6e13532edd9afa3aa52ac7964289d1a9c1
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7
with:
images: |
akashsingh04/haal_samachar
ghcr.io/${{ github.repository }}
- name: Build and push Docker images
id: push
uses: docker/build-push-action@3b5e8027fcad23fda98b2e3ac259d8d67585f671
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}


- name: Generate artifact attestation
uses: actions/attest-build-provenance@v1
with:
subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME}}
subject-digest: ${{ steps.push.outputs.digest }}
push-to-registry: true

18 changes: 11 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
## HaalSamachar : A Blog Website built with GoLang+Gin in the backend and NextJs+TypeScript in the frontend with PostgreSQL powered database.
## HaalSamachar : A Blog Website built with GoLang with multiple services which include a graphQL API using gqlgen and three REST APIs built using Gin and NextJs+TypeScript in the frontend with PostgreSQL powered database, containerized using Docker and deployed using Kubernetes.

This GoLang application is designed to serve as a blog website that implements multiple services for separate containerization and scaling.

## Features

- **Blog Website**: The application provides basic functionalities of a blog website, including creating, reading, updating, and deleting blog posts.
- **Multiple Services**: The application is divided into multiple services, each responsible for specific functionalities such as user authentication, post management, and comment handling.
- **Contract Testing**: Utilizes contract testing to verify the interactions and agreements between different services, ensuring that they work together seamlessly.
- **Modular Design**: Built with a modular design approach, making it easy to add new services or modify existing ones without impacting the entire application.
- graphQL server using gqlgen , rest server using gin mongo, docker, kubernetes , nextjs ssr.
- Write about CI/CD

Tech: GO : gin and gqlgen
Docker and kubernetes
Nextjs : Ts and tailwind
Posgresql
firebase auth

## Getting Started

Expand All @@ -29,7 +31,9 @@ To get started with this application, follow these steps:
5. **Access the Application**: Once the services are running, access the blog website through your web browser at respective localhost ports [ 8081 , 8082 , 8083 , 8084 ].


## API Documentation
## API Documentation

refactor this t0o two files: graphQL api docs and rest api docs

#### The API is deployed on 4 different services on Render (Thank the lord for their free tier)

Expand Down
175 changes: 92 additions & 83 deletions frontend/app/blogs/[blogid]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,124 +1,134 @@
'use client'
import React, { useEffect, useState } from 'react';
import Header from '../../components/Header';
import { usePathname } from 'next/navigation'
import Comments from '../../components/Comments'
import CommentForm from '../../components/CommentForm'
import Markdown from 'react-markdown'
import { usePathname } from 'next/navigation';
import Comments from '../../components/Comments';
import CommentForm from '../../components/CommentForm';
import Markdown from 'react-markdown';
import Likes from '@/app/components/Likes';
import DeleteDialogueBox from "../../components/DeleteDialogueBox";
import { auth } from "../../firebase";
import { onAuthStateChanged } from "firebase/auth";
import DeleteDialogueBox from '../../components/DeleteDialogueBox';
import { auth } from '../../firebase';
import { onAuthStateChanged } from 'firebase/auth';
import { useQuery, gql } from '@apollo/client';
import SpotifyCard from '@/app/components/SpotifyCard';

//REST API URL
const usersAPI = process.env.NEXT_PUBLIC_USERS_API_URL;
const blogsAPI = process.env.NEXT_PUBLIC_BLOGS_API_URL;

const formatDate = (timestamp: string): string => {
const date = new Date(timestamp);
const day = date.getDate();
const month = date.toLocaleString("default", { month: "short" });
const year = date.getFullYear().toString().slice(-2);
return `${day} ${month} ${year}`;
};
const getUserById = async (id: number) => {
const response = await fetch(`${usersAPI}/users/${id}`);
const data = await response.json();
return data;
}
//GraphQL queries
const GET_BLOG_BY_ID = gql`
query GetBlogById($blogId: ID!) {
blogPost(BlogID: $blogId) {
id
title
content
created_at
subtitle
image
uploadedImageLink
spotifyLink
user {
id
username
}
}
}
`;

const GET_USER_BY_ID = gql`
query GetUserById($userId: ID!) {
user(UserID: $userId) {
id
username
}
}
`;

const Page = () => {
const [isVisibleLikes, setIsVisibleLikes] = useState(true);
const [isVisibleDeleteButton, setIsVisibleDeleteButton] = useState(true);
const [isVisibleCommentsSection, setIsVisibleCommentsSection] = useState(true);
const [loggedInUserId, setLoggedInUserId] = useState<any>(null);

const [isVisible, setIsVisible] = useState(false);
const pathname = usePathname();
const blogid = pathname.split('/').pop();

// Fetch blog post data
const { loading: blogLoading, error: blogError, data: blogData } = useQuery(GET_BLOG_BY_ID, {
variables: { blogId: blogid },
});

// Fetch user data
const { loading: userLoading, error: userError, data: userData } = useQuery(GET_USER_BY_ID, {
variables: { userId: blogData?.blogPost?.user?.id },
});

useEffect(() => {
onAuthStateChanged(auth, async (user) => {
if (!user) {
setIsVisibleLikes(false);
setIsVisibleDeleteButton(false);
setIsVisibleCommentsSection(false);
} else {
console.log('User logged in successfully');
try {
const userEmail = user.email;
const userIdResponse = await fetch(`${usersAPI}/users/email/${userEmail}`);
const userIdData = await userIdResponse.json();
const userid = userIdData.ID;
setLoggedInUserId(userid);
setIsVisibleLikes(true);
setIsVisibleDeleteButton(true);
setIsVisibleCommentsSection(true);
} catch (error) {
console.error(error);
}
}
else{
console.log("User logged in successfully");
try{
const userEmail = user.email;
const userId = await fetch(`${usersAPI}/users/email/${userEmail}`, {
method: "GET",
headers: {
"Content-Type": "application/json",
},
});
const data = await userId.json();
const userid = data.ID;
setLoggedInUserId(userid);

setIsVisibleLikes(true);
setIsVisibleDeleteButton(true);
setIsVisibleCommentsSection(true);
} catch (error) {
console.error(error);
}
}
});
} , [auth])

const pathname = usePathname();
const blogid = pathname.split('/').pop();
const [blog, setBlog] = useState<any>(null);
const [user, setUser] = useState<any>(null);
const [userId, setUserId] = useState<any>(null);
const [isVisible, setIsVisible] = useState(false);
}, [auth]);

const openDelete = () => {
setIsVisible(true);
}
if (blogLoading || userLoading) return <div>Loading...</div>;
if (blogError || userError) return <div>Error: {blogError && blogError.message || userError && userError.message}</div>;

useEffect(() => {
const fetchBlog = async () => {
try {
const response = await fetch(`${blogsAPI}/blogs/${blogid}`);
if (!response.ok) {
throw new Error('Failed to fetch blog');
}
const data = await response.json();
console.log("Blog data", data)
setBlog(data);
setUserId(data.user_id);
const user = await getUserById(data.user_id);
setUser(user);
} catch (error) {
console.error(error);
}
};
const blog = blogData.blogPost;
const user = userData.user;

if (blogid) {
fetchBlog();
}
}, [blogid]);
// Function to open delete dialogue
const openDelete = () => {
setIsVisible(true);
};

if (!blog) {
return <div>
<Header bgImage='' heading='Loading' subheading='' ></Header>
</div>;
}
// Format date function
const formatDate = (timestamp : any) => {
const date = new Date(timestamp);
const day = date.getDate();
const month = date.toLocaleString('default', { month: 'short' });
const year = date.getFullYear().toString().slice(-2);
return `${day} ${month} ${year}`;
};

const formattedDate = formatDate(blog.created_at);

return (
<div>
<Header bgImage={blog.image} heading={blog.title} subheading={blog.subtitle} />
<div className="container mt-12 mx-auto p-4 px-10 border-x-4 flex justify-center flex-col items-center border-bt-navy">
<Markdown className="text-center text-2xl ">{blog.content}</Markdown>
<Markdown className="text-center text-2xl">{blog.content}</Markdown>
{isVisible && <DeleteDialogueBox
blogId={blog.id}
userId={loggedInUserId}
isBlog={true}
onClose={() => setIsVisible(false)}
/>}
<img src={blog.uploadedImageLink} alt={blog.title} width={500} height={500} className='m-8 transform scale-100 hover:scale-110 transition duration-300 ease-in-out' />
<img
src={blog.uploadedImageLink}
alt={blog.title}
width={500}
height={500}
className="m-8 transform scale-100 hover:scale-110 transition duration-300 ease-in-out"
/>
<div className="flex items-center justify-end text-right mt-4 flex-col">
<p className="text-bt-teal">Written By: {user?.Username}</p>
<p className="text-bt-teal">Written By: {user?.username}</p>
<p className="text-bt-teal">Published On: {formattedDate}</p>
<div className="flex items-center">
{isVisibleLikes && <Likes id={blog.id} />}
Expand All @@ -145,7 +155,6 @@ const Page = () => {
</div>
</div>
);

};

export default Page;
9 changes: 9 additions & 0 deletions frontend/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,18 @@ import { Dosis } from "next/font/google";
import "./globals.css";
import Footer from "./components/Footer";
import { EdgeStoreProvider } from "./lib/edgestore";
import { ApolloClient, InMemoryCache, ApolloProvider } from '@apollo/client';

const dosis = Dosis({
subsets: ["latin"],
weight: "600"
} );

const client = new ApolloClient({
uri: process.env.NEXT_PUBLIC_GRAPHQL_API_URL,
cache: new InMemoryCache()
});

export const metadata: Metadata = {
title: "HaalSamachar",
description: "Just the place you need to vent",
Expand All @@ -22,9 +29,11 @@ export default function RootLayout({
<html lang="en">

<body className={dosis.className}>
{/* <ApolloProvider client={client}> */}
<EdgeStoreProvider>
{children}
</EdgeStoreProvider>
{/* </ApolloProvider> */}
<Footer />
</body>

Expand Down
Loading

0 comments on commit e4a5068

Please sign in to comment.