diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 553c6a7..95df7d6 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -8,15 +8,15 @@ on: jobs: build: - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest steps: - name: checkout - uses: actions/checkout@v2.0.0 + uses: actions/checkout@v3 - - name: setup .net core - uses: actions/setup-dotnet@v1.7.2 + - name: Setup dotnet + uses: actions/setup-dotnet@v3 with: - dotnet-version: 6.0.100 + dotnet-version: '8.*' - name: build run: dotnet build diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2f44c62..58b1d22 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -22,7 +22,7 @@ on: jobs: push_to_registry: - runs-on: ubuntu-22.04 + runs-on: ubuntu-latest if: ${{ github.event.workflow_run.conclusion != 'failure' }} steps: # Checkout latest or specific tag @@ -50,7 +50,7 @@ jobs: fi; - name: Login to GitHub Container Registry - uses: docker/login-action@v2 + uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.actor }} @@ -58,79 +58,10 @@ jobs: # Build and push new docker image, skip for manual redeploy other than 'latest' - name: Build and push Docker images - uses: docker/build-push-action@v3 + uses: docker/build-push-action@v6 if: ${{ github.event.inputs.version == '' || github.event.inputs.version == 'latest' }} with: file: Dockerfile context: . push: true tags: ghcr.io/${{ env.image_repository_name }}:${{ env.TAG_NAME }} - - deploy_via_ssh: - needs: push_to_registry - runs-on: ubuntu-22.04 - if: ${{ github.event.workflow_run.conclusion != 'failure' }} - steps: - # Checkout latest or specific tag - - name: checkout - if: ${{ github.event.inputs.version == '' || github.event.inputs.version == 'latest' }} - uses: actions/checkout@v3 - - name: checkout tag - if: ${{ github.event.inputs.version != '' && github.event.inputs.version != 'latest' }} - uses: actions/checkout@v3 - with: - ref: refs/tags/${{ github.event.inputs.version }} - - - name: repository name fix and env - run: | - echo "image_repository_name=$(echo ${{ github.repository }} | tr '[:upper:]' '[:lower:]')" >> $GITHUB_ENV - echo "domain=${{ secrets.DEPLOY_HOST }}" >> $GITHUB_ENV - echo "letsencrypt_email=${{ secrets.LETSENCRYPT_EMAIL }}" >> $GITHUB_ENV - echo "TAG_NAME=latest" >> $GITHUB_ENV - if [ "${{ github.event.release.tag_name }}" != "" ]; then - echo "TAG_NAME=${{ github.event.release.tag_name }}" >> $GITHUB_ENV - fi; - if [ "${{ github.event.inputs.version }}" != "" ]; then - echo "TAG_NAME=${{ github.event.inputs.version }}" >> $GITHUB_ENV - fi; - - # Populate docker-compose.yml with variables from build process, including TAG_NAME. - - name: docker-compose file prep - uses: danielr1996/envsubst-action@1.0.0 - env: - RELEASE_VERSION: ${{ env.TAG_NAME }} - IMAGE_REPO: ${{ env.image_repository_name }} - APP_NAME: ${{ github.event.repository.name }} - HOST_DOMAIN: ${{ env.domain }} - LETSENCRYPT_EMAIL: ${{ env.letsencrypt_email }} - with: - input: .deploy/docker-compose-template.yml - output: .deploy/${{ github.event.repository.name }}-docker-compose.yml - - # Copy only the docker-compose.yml to remote server home folder - - name: copy compose file via scp - uses: appleboy/scp-action@v0.1.3 - with: - host: ${{ secrets.DEPLOY_HOST }} - username: ${{ secrets.DEPLOY_USERNAME }} - port: 22 - key: ${{ secrets.DEPLOY_KEY }} - source: ".deploy/${{ github.event.repository.name }}-docker-compose.yml" - target: "~/" - - # Deploy Docker image with ServiceStack application using `docker compose up` remotely - - name: remote docker-compose up via ssh - uses: appleboy/ssh-action@v0.1.5 - env: - APPTOKEN: ${{ secrets.GITHUB_TOKEN }} - USERNAME: ${{ secrets.DEPLOY_USERNAME }} - with: - host: ${{ secrets.DEPLOY_HOST }} - username: ${{ secrets.DEPLOY_USERNAME }} - key: ${{ secrets.DEPLOY_KEY }} - port: 22 - envs: APPTOKEN,USERNAME - script: | - echo $APPTOKEN | docker login ghcr.io -u $USERNAME --password-stdin - docker-compose -f ~/.deploy/${{ github.event.repository.name }}-docker-compose.yml pull - docker-compose -f ~/.deploy/${{ github.event.repository.name }}-docker-compose.yml up -d \ No newline at end of file diff --git a/.kamal/hooks/docker-setup.sample b/.kamal/hooks/docker-setup.sample new file mode 100755 index 0000000..2fb07d7 --- /dev/null +++ b/.kamal/hooks/docker-setup.sample @@ -0,0 +1,3 @@ +#!/bin/sh + +echo "Docker set up on $KAMAL_HOSTS..." diff --git a/.kamal/hooks/post-deploy.sample b/.kamal/hooks/post-deploy.sample new file mode 100755 index 0000000..75efafc --- /dev/null +++ b/.kamal/hooks/post-deploy.sample @@ -0,0 +1,14 @@ +#!/bin/sh + +# A sample post-deploy hook +# +# These environment variables are available: +# KAMAL_RECORDED_AT +# KAMAL_PERFORMER +# KAMAL_VERSION +# KAMAL_HOSTS +# KAMAL_ROLE (if set) +# KAMAL_DESTINATION (if set) +# KAMAL_RUNTIME + +echo "$KAMAL_PERFORMER deployed $KAMAL_VERSION to $KAMAL_DESTINATION in $KAMAL_RUNTIME seconds" diff --git a/.kamal/hooks/post-proxy-reboot.sample b/.kamal/hooks/post-proxy-reboot.sample new file mode 100755 index 0000000..1435a67 --- /dev/null +++ b/.kamal/hooks/post-proxy-reboot.sample @@ -0,0 +1,3 @@ +#!/bin/sh + +echo "Rebooted kamal-proxy on $KAMAL_HOSTS" diff --git a/.kamal/hooks/pre-build.sample b/.kamal/hooks/pre-build.sample new file mode 100755 index 0000000..f87d811 --- /dev/null +++ b/.kamal/hooks/pre-build.sample @@ -0,0 +1,51 @@ +#!/bin/sh + +# A sample pre-build hook +# +# Checks: +# 1. We have a clean checkout +# 2. A remote is configured +# 3. The branch has been pushed to the remote +# 4. The version we are deploying matches the remote +# +# These environment variables are available: +# KAMAL_RECORDED_AT +# KAMAL_PERFORMER +# KAMAL_VERSION +# KAMAL_HOSTS +# KAMAL_ROLE (if set) +# KAMAL_DESTINATION (if set) + +if [ -n "$(git status --porcelain)" ]; then + echo "Git checkout is not clean, aborting..." >&2 + git status --porcelain >&2 + exit 1 +fi + +first_remote=$(git remote) + +if [ -z "$first_remote" ]; then + echo "No git remote set, aborting..." >&2 + exit 1 +fi + +current_branch=$(git branch --show-current) + +if [ -z "$current_branch" ]; then + echo "Not on a git branch, aborting..." >&2 + exit 1 +fi + +remote_head=$(git ls-remote $first_remote --tags $current_branch | cut -f1) + +if [ -z "$remote_head" ]; then + echo "Branch not pushed to remote, aborting..." >&2 + exit 1 +fi + +if [ "$KAMAL_VERSION" != "$remote_head" ]; then + echo "Version ($KAMAL_VERSION) does not match remote HEAD ($remote_head), aborting..." >&2 + exit 1 +fi + +exit 0 diff --git a/.kamal/hooks/pre-connect.sample b/.kamal/hooks/pre-connect.sample new file mode 100755 index 0000000..18e61d7 --- /dev/null +++ b/.kamal/hooks/pre-connect.sample @@ -0,0 +1,47 @@ +#!/usr/bin/env ruby + +# A sample pre-connect check +# +# Warms DNS before connecting to hosts in parallel +# +# These environment variables are available: +# KAMAL_RECORDED_AT +# KAMAL_PERFORMER +# KAMAL_VERSION +# KAMAL_HOSTS +# KAMAL_ROLE (if set) +# KAMAL_DESTINATION (if set) +# KAMAL_RUNTIME + +hosts = ENV["KAMAL_HOSTS"].split(",") +results = nil +max = 3 + +elapsed = Benchmark.realtime do + results = hosts.map do |host| + Thread.new do + tries = 1 + + begin + Socket.getaddrinfo(host, 0, Socket::AF_UNSPEC, Socket::SOCK_STREAM, nil, Socket::AI_CANONNAME) + rescue SocketError + if tries < max + puts "Retrying DNS warmup: #{host}" + tries += 1 + sleep rand + retry + else + puts "DNS warmup failed: #{host}" + host + end + end + + tries + end + end.map(&:value) +end + +retries = results.sum - hosts.size +nopes = results.count { |r| r == max } + +puts "Prewarmed %d DNS lookups in %.2f sec: %d retries, %d failures" % [ hosts.size, elapsed, retries, nopes ] diff --git a/.kamal/hooks/pre-deploy.sample b/.kamal/hooks/pre-deploy.sample new file mode 100755 index 0000000..1b280c7 --- /dev/null +++ b/.kamal/hooks/pre-deploy.sample @@ -0,0 +1,109 @@ +#!/usr/bin/env ruby + +# A sample pre-deploy hook +# +# Checks the Github status of the build, waiting for a pending build to complete for up to 720 seconds. +# +# Fails unless the combined status is "success" +# +# These environment variables are available: +# KAMAL_RECORDED_AT +# KAMAL_PERFORMER +# KAMAL_VERSION +# KAMAL_HOSTS +# KAMAL_COMMAND +# KAMAL_SUBCOMMAND +# KAMAL_ROLE (if set) +# KAMAL_DESTINATION (if set) + +# Only check the build status for production deployments +if ENV["KAMAL_COMMAND"] == "rollback" || ENV["KAMAL_DESTINATION"] != "production" + exit 0 +end + +require "bundler/inline" + +# true = install gems so this is fast on repeat invocations +gemfile(true, quiet: true) do + source "https://rubygems.org" + + gem "octokit" + gem "faraday-retry" +end + +MAX_ATTEMPTS = 72 +ATTEMPTS_GAP = 10 + +def exit_with_error(message) + $stderr.puts message + exit 1 +end + +class GithubStatusChecks + attr_reader :remote_url, :git_sha, :github_client, :combined_status + + def initialize + @remote_url = `git config --get remote.origin.url`.strip.delete_prefix("https://github.com/") + @git_sha = `git rev-parse HEAD`.strip + @github_client = Octokit::Client.new(access_token: ENV["GITHUB_TOKEN"]) + refresh! + end + + def refresh! + @combined_status = github_client.combined_status(remote_url, git_sha) + end + + def state + combined_status[:state] + end + + def first_status_url + first_status = combined_status[:statuses].find { |status| status[:state] == state } + first_status && first_status[:target_url] + end + + def complete_count + combined_status[:statuses].count { |status| status[:state] != "pending"} + end + + def total_count + combined_status[:statuses].count + end + + def current_status + if total_count > 0 + "Completed #{complete_count}/#{total_count} checks, see #{first_status_url} ..." + else + "Build not started..." + end + end +end + + +$stdout.sync = true + +puts "Checking build status..." +attempts = 0 +checks = GithubStatusChecks.new + +begin + loop do + case checks.state + when "success" + puts "Checks passed, see #{checks.first_status_url}" + exit 0 + when "failure" + exit_with_error "Checks failed, see #{checks.first_status_url}" + when "pending" + attempts += 1 + end + + exit_with_error "Checks are still pending, gave up after #{MAX_ATTEMPTS * ATTEMPTS_GAP} seconds" if attempts == MAX_ATTEMPTS + + puts checks.current_status + sleep(ATTEMPTS_GAP) + checks.refresh! + end +rescue Octokit::NotFound + exit_with_error "Build status could not be found" +end diff --git a/.kamal/hooks/pre-proxy-reboot.sample b/.kamal/hooks/pre-proxy-reboot.sample new file mode 100755 index 0000000..061f805 --- /dev/null +++ b/.kamal/hooks/pre-proxy-reboot.sample @@ -0,0 +1,3 @@ +#!/bin/sh + +echo "Rebooting kamal-proxy on $KAMAL_HOSTS..." diff --git a/.kamal/secrets b/.kamal/secrets new file mode 100644 index 0000000..690aa96 --- /dev/null +++ b/.kamal/secrets @@ -0,0 +1,18 @@ +# Secrets defined here are available for reference under registry/password, env/secret, builder/secrets, +# and accessories/*/env/secret in config/deploy.yml. All secrets should be pulled from either +# password manager, ENV, or a file. DO NOT ENTER RAW CREDENTIALS HERE! This file needs to be safe for git. + +# Option 1: Read secrets from the environment +KAMAL_REGISTRY_PASSWORD=$KAMAL_REGISTRY_PASSWORD +KAMAL_REGISTRY_USERNAME=$KAMAL_REGISTRY_USERNAME + +# Option 2: Read secrets via a command +# RAILS_MASTER_KEY=$(cat config/master.key) + +# Option 3: Read secrets via kamal secrets helpers +# These will handle logging in and fetching the secrets in as few calls as possible +# There are adapters for 1Password, LastPass + Bitwarden +# +# SECRETS=$(kamal secrets fetch --adapter 1password --account my-account --from MyVault/MyItem KAMAL_REGISTRY_PASSWORD RAILS_MASTER_KEY) +# KAMAL_REGISTRY_PASSWORD=$(kamal secrets extract KAMAL_REGISTRY_PASSWORD $SECRETS) +# RAILS_MASTER_KEY=$(kamal secrets extract RAILS_MASTER_KEY $SECRETS) diff --git a/Dockerfile b/Dockerfile index 9e6ae4b..8246b38 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build +FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build WORKDIR /source COPY . . @@ -7,7 +7,9 @@ RUN dotnet restore ./src/ WORKDIR /source/src RUN dotnet publish -c release -o /app --no-restore -FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS runtime +FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS runtime +LABEL service="sharpscript" + WORKDIR /app COPY --from=build /app ./ ENTRYPOINT ["dotnet", "SharpScript.dll"] \ No newline at end of file diff --git a/config/deploy.yml b/config/deploy.yml new file mode 100644 index 0000000..ff69491 --- /dev/null +++ b/config/deploy.yml @@ -0,0 +1,103 @@ +# Name of your application. Used to uniquely configure containers. +service: sharpscript + +# Name of the container image. +image: servicestack/sharpscript + +# Deploy to these servers. +servers: + web: + - 5.78.128.205 + # job: + # hosts: + # - 192.168.0.1 + # cmd: bin/jobs + +# Enable SSL auto certification via Let's Encrypt and allow for multiple apps on a single web server. +# Remove this section when using multiple web servers and ensure you terminate SSL at your load balancer. +# +# Note: If using Cloudflare, set encryption mode in SSL/TLS setting to "Full" to enable CF-to-app encryption. +proxy: + ssl: true + host: sharpscript.net + # Proxy connects to your container on port 80 by default. + app_port: 8080 + healthcheck: + interval: 3 + path: /metadata + timeout: 3 + +# Credentials for your image host. +registry: + # Specify the registry server, if you're not using Docker Hub + server: ghcr.io + username: + - KAMAL_REGISTRY_USERNAME + + # Always use an access token rather than real password (pulled from .kamal/secrets). + password: + - KAMAL_REGISTRY_PASSWORD + +# Configure builder setup. +builder: + arch: amd64 + +# Inject ENV variables into containers (secrets come from .kamal/secrets). +# +# env: +# clear: +# DB_HOST: 192.168.0.2 +# secret: +# - RAILS_MASTER_KEY + +# Aliases are triggered with "bin/kamal ". You can overwrite arguments on invocation: +# "bin/kamal logs -r job" will tail logs from the first server in the job section. +# +# aliases: +# shell: app exec --interactive --reuse "bash" + +# Use a different ssh user than root +# +# ssh: +# user: app + +# Use a persistent storage volume. +# +volumes: + - "/opt/docker/sharpscript/App_Data:/app/App_Data" + +# Bridge fingerprinted assets, like JS and CSS, between versions to avoid +# hitting 404 on in-flight requests. Combines all files from new and old +# version inside the asset_path. +# +# asset_path: /app/public/assets + +# Configure rolling deploys by setting a wait time between batches of restarts. +# +# boot: +# limit: 10 # Can also specify as a percentage of total hosts, such as "25%" +# wait: 2 + +# Use accessory services (secrets come from .kamal/secrets). +# +# accessories: +# db: +# image: mysql:8.0 +# host: 192.168.0.2 +# port: 3306 +# env: +# clear: +# MYSQL_ROOT_HOST: '%' +# secret: +# - MYSQL_ROOT_PASSWORD +# files: +# - config/mysql/production.cnf:/etc/mysql/my.cnf +# - db/production.sql:/docker-entrypoint-initdb.d/setup.sql +# directories: +# - data:/var/lib/mysql +# redis: +# image: valkey/valkey:8 +# host: 192.168.0.2 +# port: 6379 +# directories: +# - data:/data diff --git a/src/Program.cs b/src/Program.cs index 3b7cc15..f816e5b 100644 --- a/src/Program.cs +++ b/src/Program.cs @@ -1,24 +1,25 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.AspNetCore; -using Microsoft.AspNetCore.Hosting; +var builder = WebApplication.CreateBuilder(args); -namespace SharpScript -{ - public class Program - { - public static void Main(string[] args) - { - BuildWebHost(args).Run(); - } +var cultureInfo = new System.Globalization.CultureInfo("en-US"); +System.Globalization.CultureInfo.DefaultThreadCurrentCulture = cultureInfo; +System.Globalization.CultureInfo.DefaultThreadCurrentUICulture = cultureInfo; + +var app = builder.Build(); - public static IWebHost BuildWebHost(string[] args) => - WebHost.CreateDefaultBuilder(args) - .UseStartup() - .UseUrls("http://*:5000/") - .Build(); - } +// Configure the HTTP request pipeline. +if (app.Environment.IsDevelopment()) +{ +} +else +{ + app.UseExceptionHandler("/Error"); + // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. + app.UseHsts(); } + +app.UseHttpsRedirection(); +app.UseStaticFiles(); + +app.UseServiceStack(new AppHost()); + +app.Run(); diff --git a/src/SharpScript.csproj b/src/SharpScript.csproj index 11daba3..e4d3220 100644 --- a/src/SharpScript.csproj +++ b/src/SharpScript.csproj @@ -1,26 +1,39 @@  - - net6.0 + + net8.0 SharpScript SharpScript + enable + enable + true + DefaultContainer + + + + + + + + + - + - - - - - - - - - - + + + + + + + + + + diff --git a/src/Startup.cs b/src/Startup.cs.rename similarity index 100% rename from src/Startup.cs rename to src/Startup.cs.rename