Compare commits
8 Commits
14f9eb1235
...
v0.0.1-rc1
| Author | SHA1 | Date | |
|---|---|---|---|
| 72fd878f99 | |||
| 69762d22ba | |||
| 1b1e776031 | |||
| 107a3b1906 | |||
| b6bdfd8f11 | |||
| 98138ad644 | |||
| 671198cf98 | |||
| cf37c2ad12 |
@@ -5,39 +5,33 @@ on:
|
||||
- 'v*' # v1.2.3, v1.2.3-rc.1, etc.
|
||||
|
||||
jobs:
|
||||
build-and-publish-image:
|
||||
publish-image:
|
||||
name: Build & Publish Image
|
||||
runs-on: [self-hosted, linux, x64, docker] # NAS or any Linux runner with Docker
|
||||
runs-on: [self-hosted, linux, x64, docker]
|
||||
container:
|
||||
image: ghcr.io/catthehacker/ubuntu:act-latest
|
||||
options: >-
|
||||
--privileged
|
||||
env:
|
||||
REGISTRY: ${{ secrets.REGISTRY_HOST }}
|
||||
OWNER_REPO: ${{ github.repository }}
|
||||
DOCKERFILE: JSMR.Api/Dockerfile
|
||||
CONTEXT: .
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-dotnet@v4
|
||||
with: { dotnet-version: '9.0.x' }
|
||||
|
||||
- run: dotnet restore
|
||||
- run: dotnet publish JSMR/JSMR.Api/JSMR.Api.csproj -c Release -o publish
|
||||
|
||||
- name: Create Dockerfile.ci
|
||||
run: |
|
||||
cat > Dockerfile.ci <<'EOF'
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:9.0
|
||||
WORKDIR /app
|
||||
COPY publish/ ./
|
||||
ENV ASPNETCORE_URLS=http://+:8080
|
||||
EXPOSE 8080
|
||||
ENTRYPOINT ["dotnet", "JSMR.Api.dll"]
|
||||
EOF
|
||||
|
||||
- name: Normalize image name
|
||||
id: names
|
||||
- name: Compute image name + tag + prerelease flag
|
||||
id: meta
|
||||
shell: bash
|
||||
run: |
|
||||
IMAGE_LC="$(echo "${REGISTRY}/${OWNER_REPO}" | tr '[:upper:]' '[:lower:]')"
|
||||
echo "image=${IMAGE_LC}" >> "$GITHUB_OUTPUT"
|
||||
# tag derived from ref, e.g. refs/tags/v1.3.0 -> v1.3.0
|
||||
echo "tag=${GITHUB_REF_NAME}" >> "$GITHUB_OUTPUT"
|
||||
if [[ "${GITHUB_REF_NAME}" == *"-"* ]]; then
|
||||
echo "is_prerelease=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "is_prerelease=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Docker login
|
||||
run: |
|
||||
@@ -46,52 +40,131 @@ jobs:
|
||||
-u "${{ secrets.REGISTRY_USER }}" \
|
||||
--password-stdin
|
||||
|
||||
- name: Build image
|
||||
run: docker build -f Dockerfile.ci -t "${{ steps.names.outputs.image }}:${{ steps.names.outputs.tag }}" .
|
||||
- name: Enable BuildKit
|
||||
run: echo "DOCKER_BUILDKIT=1" >> $GITHUB_ENV
|
||||
|
||||
- name: Push image
|
||||
- name: Build (from your API Dockerfile)
|
||||
run: |
|
||||
docker push "${{ steps.names.outputs.image }}:${{ steps.names.outputs.tag }}"
|
||||
docker tag "${{ steps.names.outputs.image }}:${{ steps.names.outputs.tag }}" "${{ steps.names.outputs.image }}:latest"
|
||||
docker push "${{ steps.names.outputs.image }}:latest"
|
||||
docker build \
|
||||
-f "$DOCKERFILE" \
|
||||
-t "${{ steps.meta.outputs.image }}:${{ steps.meta.outputs.tag }}" \
|
||||
--label "org.opencontainers.image.source=${{ github.server_url }}/${{ github.repository }}" \
|
||||
--label "org.opencontainers.image.version=${{ steps.meta.outputs.tag }}" \
|
||||
"$CONTEXT"
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: api-publish-${{ steps.names.outputs.tag }}
|
||||
path: publish
|
||||
- name: Push (tagged)
|
||||
run: docker push "${{ steps.meta.outputs.image }}:${{ steps.meta.outputs.tag }}"
|
||||
|
||||
- name: Also tag/push :latest for stable tags only
|
||||
if: ${{ steps.meta.outputs.is_prerelease == 'false' }}
|
||||
run: |
|
||||
docker tag "${{ steps.meta.outputs.image }}:${{ steps.meta.outputs.tag }}" "${{ steps.meta.outputs.image }}:latest"
|
||||
docker push "${{ steps.meta.outputs.image }}:latest"
|
||||
|
||||
# Optional: export a tiny text artifact with the image refs (handy for debugging)
|
||||
- name: Emit image refs
|
||||
run: |
|
||||
echo "${{ steps.meta.outputs.image }}:${{ steps.meta.outputs.tag }}" > image.txt
|
||||
if [ "${{ steps.meta.outputs.is_prerelease }}" = "false" ]; then
|
||||
echo "${{ steps.meta.outputs.image }}:latest" >> image.txt
|
||||
fi
|
||||
|
||||
create-gitea-release:
|
||||
name: Create Gitea Release
|
||||
needs: build-and-publish-image
|
||||
needs: publish-image
|
||||
runs-on: [self-hosted, linux, x64, docker]
|
||||
container:
|
||||
image: ghcr.io/catthehacker/ubuntu:act-latest
|
||||
options: >-
|
||||
--privileged
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
# simplest notes: use tag message (annotated) and attach image refs
|
||||
- name: Generate basic notes
|
||||
id: notes
|
||||
- name: Install jq (for safe JSON quoting)
|
||||
run: |
|
||||
echo "## Release $GITHUB_REF_NAME" > REL_NOTES.md
|
||||
echo "" >> REL_NOTES.md
|
||||
echo "- Image: ${{ secrets.REGISTRY_HOST }}/${{ github.repository }}:${GITHUB_REF_NAME}" >> REL_NOTES.md
|
||||
echo "- Latest: ${{ secrets.REGISTRY_HOST }}/${{ github.repository }}:latest" >> REL_NOTES.md
|
||||
apt-get update -y
|
||||
apt-get install -y jq curl
|
||||
|
||||
# Gitea has a create-release action too; if not, use API curl
|
||||
- name: Create Release via API
|
||||
- name: Prepare release notes
|
||||
id: notes
|
||||
env:
|
||||
GITEA_BASE: ${{ secrets.GITEA_BASE_URL }} # e.g. https://gitea.example.com
|
||||
TOKEN: ${{ secrets.GITEA_TOKEN }} # personal access token w/ repo write
|
||||
REGISTRY: ${{ secrets.REGISTRY_HOST }}
|
||||
REPO: ${{ github.repository }}
|
||||
shell: bash
|
||||
run: |
|
||||
TAG="${GITHUB_REF_NAME}"
|
||||
IMG="${REGISTRY}/${REPO}:${TAG}"
|
||||
cat > REL_NOTES.md <<EOF
|
||||
## ${TAG}
|
||||
|
||||
**Images**
|
||||
- \`${IMG}\`
|
||||
EOF
|
||||
# Add :latest mention only for stable tags
|
||||
if [[ "$TAG" != *"-"* ]]; then
|
||||
echo "- \`${REGISTRY}/${REPO}:latest\`" >> REL_NOTES.md
|
||||
fi
|
||||
|
||||
- name: Create or Update Release via Gitea API
|
||||
env:
|
||||
GITEA_BASE: ${{ secrets.GITEA_BASE_URL }}
|
||||
TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||
shell: bash
|
||||
run: |
|
||||
TAG="${GITHUB_REF_NAME}"
|
||||
BODY="$(cat REL_NOTES.md)"
|
||||
IS_PRERELEASE=false
|
||||
[[ "$TAG" == *"-"* ]] && IS_PRERELEASE=true
|
||||
|
||||
# Try to get existing release by tag
|
||||
set +e
|
||||
EXISTING=$(curl -s -H "Authorization: token ${TOKEN}" "${GITEA_BASE}/api/v1/repos/${{ github.repository }}/releases/tags/${TAG}")
|
||||
STATUS=$?
|
||||
set -e
|
||||
|
||||
if echo "$EXISTING" | jq -e .id >/dev/null 2>&1; then
|
||||
REL_ID=$(echo "$EXISTING" | jq -r .id)
|
||||
# Update
|
||||
curl -sS -X PATCH "${GITEA_BASE}/api/v1/repos/${{ github.repository }}/releases/${REL_ID}" \
|
||||
-H "Authorization: token ${TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$(jq -n --arg t "$TAG" --arg b "$BODY" --argjson pre $IS_PRERELEASE '{name:$t, body:$b, prerelease:$pre}')" >/dev/null
|
||||
else
|
||||
# Create
|
||||
curl -sS -X POST "${GITEA_BASE}/api/v1/repos/${{ github.repository }}/releases" \
|
||||
-H "Authorization: token ${TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d @- <<JSON
|
||||
{
|
||||
"tag_name": "${GITHUB_REF_NAME}",
|
||||
"name": "${GITHUB_REF_NAME}",
|
||||
"body": ${jq@JSON:- <<<'$BODY'},
|
||||
"draft": false,
|
||||
"prerelease": ${GITHUB_REF_NAME##*-rc*:+false}${GITHUB_REF_NAME##*-rc*:-true}
|
||||
}
|
||||
JSON
|
||||
-d "$(jq -n --arg t "$TAG" --arg b "$BODY" --argjson pre $IS_PRERELEASE '{tag_name:$t, name:$t, body:$b, draft:false, prerelease:$pre}')" >/dev/null
|
||||
fi
|
||||
|
||||
deploy-api:
|
||||
name: Deploy API (Production)
|
||||
needs: publish-image
|
||||
runs-on: [self-hosted, linux, x64, docker] # your Synology runner
|
||||
environment: production # optional: add env protection rules in Gitea
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Docker login (pull private image)
|
||||
run: |
|
||||
echo "${{ secrets.REGISTRY_PASSWORD }}" \
|
||||
| docker login "${{ secrets.REGISTRY_HOST }}" \
|
||||
-u "${{ secrets.REGISTRY_USER }}" \
|
||||
--password-stdin
|
||||
|
||||
- name: Render env file for compose
|
||||
run: |
|
||||
mkdir -p deploy
|
||||
cat > deploy/.env <<EOF
|
||||
REGISTRY=${{ secrets.REGISTRY_HOST }}
|
||||
IMAGE_NS=${{ github.repository }}
|
||||
TAG=${GITHUB_REF_NAME}
|
||||
DB_CONN=${{ secrets.DB_CONNECTION_STRING }}
|
||||
ALLOWED_ORIGINS=${{ secrets.ALLOWED_ORIGINS }}
|
||||
TZ=America/Detroit
|
||||
EOF
|
||||
|
||||
- name: Deploy (pull & up -d)
|
||||
run: |
|
||||
docker compose -f deploy/docker-compose.api.yml --env-file deploy/.env pull
|
||||
docker compose -f deploy/docker-compose.api.yml --env-file deploy/.env up -d
|
||||
|
||||
20
JSMR.UI.Blazor/Components/Chip.razor
Normal file
20
JSMR.UI.Blazor/Components/Chip.razor
Normal file
@@ -0,0 +1,20 @@
|
||||
@using JSMR.UI.Blazor.Enums
|
||||
|
||||
<div class="j-chip">
|
||||
@if (Graphic != null)
|
||||
{
|
||||
<Icon Graphic="@Graphic.Value" Color="@Color"></Icon>
|
||||
}
|
||||
<span>@ChildContent</span>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
[Parameter]
|
||||
public RenderFragment? ChildContent { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public Graphic? Graphic { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public ColorVarient Color { get; set; } = ColorVarient.Primary;
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
@using JSMR.Application.VoiceWorks.Queries.Search
|
||||
@using JSMR.Domain.Enums
|
||||
@using JSMR.UI.Blazor.Enums
|
||||
@using JSMR.UI.Blazor.Filters
|
||||
@using JSMR.UI.Blazor.Services
|
||||
@using System.Globalization
|
||||
@using Microsoft.AspNetCore.WebUtilities
|
||||
|
||||
<div class=@GetCardClasses(Product)>
|
||||
<div class="j-voice-work-image-container">
|
||||
@@ -33,7 +35,8 @@
|
||||
<div class="j-tags">
|
||||
@foreach (var tag in Product.Tags)
|
||||
{
|
||||
<div class="j-tag">@tag.Name</div>
|
||||
@* <div class="j-tag">@tag.Name</div> *@
|
||||
<ProductTag Tag="tag"></ProductTag>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
@@ -73,20 +76,6 @@
|
||||
<ProductIndicator Graphic="Graphic.Headphones" Color="ColorVarient.Blue" BackgroundColor="ColorVarient.Black"></ProductIndicator>
|
||||
}
|
||||
</BitStack>
|
||||
@* <div class="j-product-indicators">
|
||||
@if (Product.IsValid != true)
|
||||
{
|
||||
<ProductIndicator Graphic="Graphic.Warning" IconVarient="IconVarient.Fill" Color="ColorVarient.Orange" BackgroundColor="ColorVarient.Black"></ProductIndicator>
|
||||
}
|
||||
@if (Product.Favorite)
|
||||
{
|
||||
<ProductIndicator Graphic="Graphic.Star" Color="ColorVarient.Pink" BackgroundColor="ColorVarient.Black"></ProductIndicator>
|
||||
}
|
||||
@if (Product.HasTrial || Product.HasChobit)
|
||||
{
|
||||
<ProductIndicator Graphic="Graphic.Headphones" Color="ColorVarient.Blue" BackgroundColor="ColorVarient.Black"></ProductIndicator>
|
||||
}
|
||||
</div> *@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
28
JSMR.UI.Blazor/Components/ProductTag.razor
Normal file
28
JSMR.UI.Blazor/Components/ProductTag.razor
Normal file
@@ -0,0 +1,28 @@
|
||||
@using JSMR.Application.VoiceWorks.Queries.Search
|
||||
@using JSMR.UI.Blazor.Filters
|
||||
@using Microsoft.AspNetCore.WebUtilities
|
||||
|
||||
<a class="j-tag" @onclick="@OnClick">@Tag.Name</a>
|
||||
@* <MudChip T="string" Icon="@Icons.Material.Outlined.Tag" @onclick="@OnClick" Variant="@MudBlazor.Variant.Filled" Color="@MudBlazor.Color.Surface">@Tag.Name</MudChip> *@
|
||||
|
||||
@code {
|
||||
[Inject]
|
||||
protected NavigationManager NavigationManager { get; set; } = default!;
|
||||
|
||||
[Parameter]
|
||||
public required VoiceWorkTagItem Tag { get; set; }
|
||||
|
||||
private void OnClick()
|
||||
{
|
||||
VoiceWorkFilterState state = new()
|
||||
{
|
||||
TagIds = [Tag.TagId]
|
||||
};
|
||||
|
||||
//string basePath = new Uri(NavigationManager.Uri).GetLeftPart(UriPartial.Path);
|
||||
string basePath = new Uri(NavigationManager.Uri).GetLeftPart(UriPartial.Authority);
|
||||
string uri = QueryHelpers.AddQueryString($"{basePath}/voiceworks", state.ToQuery());
|
||||
|
||||
NavigationManager.NavigateTo(uri);
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<ServiceWorkerAssetsManifest>service-worker-assets.js</ServiceWorkerAssetsManifest>
|
||||
<RunAOTCompilation>false</RunAOTCompilation>
|
||||
<UserSecretsId>ef618c0a-0813-4cb9-a3db-81757469494a</UserSecretsId>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -608,6 +608,15 @@ code {
|
||||
font-weight: 500;*/
|
||||
}
|
||||
|
||||
/* Chips */
|
||||
.j-chip {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: .5rem;
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Icons */
|
||||
.j-icon {
|
||||
mask-repeat: no-repeat;
|
||||
|
||||
39
JSMR.UI.Blazor/wwwroot/css/font.css
Normal file
39
JSMR.UI.Blazor/wwwroot/css/font.css
Normal file
@@ -0,0 +1,39 @@
|
||||
@font-face {
|
||||
font-family: "Poppins";
|
||||
src: url("../fonts/Poppins-Regular.otf") format("openType");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Poppins";
|
||||
src: url("../fonts/Poppins-Light.otf") format("openType");
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Poppins";
|
||||
src: url("../fonts/Poppins-Medium.otf") format("openType");
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Poppins";
|
||||
src: url("../fonts/Poppins-SemiBold.otf") format("openType");
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Poppins";
|
||||
src: url("../fonts/Poppins-Bold.otf") format("openType");
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "M+ 1p";
|
||||
src: url("../fonts/mplus-1p-regular.ttf") format("trueType");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "M+ 1p";
|
||||
src: url("../fonts/mplus-1p-bold.ttf") format("trueType");
|
||||
font-weight: 700;
|
||||
}
|
||||
BIN
JSMR.UI.Blazor/wwwroot/fonts/Poppins-Bold.otf
Normal file
BIN
JSMR.UI.Blazor/wwwroot/fonts/Poppins-Bold.otf
Normal file
Binary file not shown.
BIN
JSMR.UI.Blazor/wwwroot/fonts/Poppins-Light.otf
Normal file
BIN
JSMR.UI.Blazor/wwwroot/fonts/Poppins-Light.otf
Normal file
Binary file not shown.
BIN
JSMR.UI.Blazor/wwwroot/fonts/Poppins-Medium.otf
Normal file
BIN
JSMR.UI.Blazor/wwwroot/fonts/Poppins-Medium.otf
Normal file
Binary file not shown.
BIN
JSMR.UI.Blazor/wwwroot/fonts/Poppins-Regular.otf
Normal file
BIN
JSMR.UI.Blazor/wwwroot/fonts/Poppins-Regular.otf
Normal file
Binary file not shown.
BIN
JSMR.UI.Blazor/wwwroot/fonts/Poppins-SemiBold.otf
Normal file
BIN
JSMR.UI.Blazor/wwwroot/fonts/Poppins-SemiBold.otf
Normal file
Binary file not shown.
BIN
JSMR.UI.Blazor/wwwroot/fonts/mplus-1p-bold.ttf
Normal file
BIN
JSMR.UI.Blazor/wwwroot/fonts/mplus-1p-bold.ttf
Normal file
Binary file not shown.
BIN
JSMR.UI.Blazor/wwwroot/fonts/mplus-1p-regular.ttf
Normal file
BIN
JSMR.UI.Blazor/wwwroot/fonts/mplus-1p-regular.ttf
Normal file
Binary file not shown.
@@ -24,6 +24,7 @@
|
||||
<link rel="stylesheet" href="css/radzen.css" />
|
||||
<link href="_content/Bit.BlazorUI/styles/bit.blazorui.css" rel="stylesheet" />
|
||||
<link rel="stylesheet" href="css/bit-blazor.css" />
|
||||
<link rel="stylesheet" href="css/font.css" />
|
||||
<link rel="stylesheet" href="css/app.css" />
|
||||
<link rel="stylesheet" href="css/theme-base.css" />
|
||||
<link rel="stylesheet" href="css/theme-frozen.css" />
|
||||
|
||||
18
deploy/docker-compose.api.yml
Normal file
18
deploy/docker-compose.api.yml
Normal file
@@ -0,0 +1,18 @@
|
||||
services:
|
||||
api:
|
||||
image: ${REGISTRY}/${IMAGE_NS}:${TAG}
|
||||
container_name: jsmr-api
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
ASPNETCORE_URLS: http://+:8080
|
||||
ASPNETCORE_ENVIRONMENT: Production
|
||||
ConnectionStrings__Default: ${DB_CONN}
|
||||
CORS__AllowedOrigins: ${ALLOWED_ORIGINS}
|
||||
TZ: ${TZ:-America/Detroit}
|
||||
ports:
|
||||
- "8080:8080" # or remove and put behind your Synology reverse-proxy
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "-qO-", "http://localhost:8080/health"]
|
||||
interval: 10s
|
||||
timeout: 3s
|
||||
retries: 12
|
||||
Reference in New Issue
Block a user