Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
96 changes: 96 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
version: 2

# Coverage note: PlanViewer.sln does NOT contain server/PlanShare, PlanViewer.Ssms,
# or PlanViewer.Ssms.Installer, so any solution-level scan silently skips them.
# Every project directory is therefore listed explicitly below. If a new .csproj
# is added anywhere, add its directory here too.
updates:
- package-ecosystem: nuget
directories:
- "/src/PlanViewer.App"
- "/src/PlanViewer.Core"
- "/src/PlanViewer.Cli"
- "/src/PlanViewer.Web"
- "/src/PlanViewer.Ssms"
- "/src/PlanViewer.Ssms.Installer"
- "/tests/PlanViewer.Core.Tests"
- "/server/PlanShare"
schedule:
interval: weekly
day: monday
time: "06:00"
timezone: America/New_York
open-pull-requests-limit: 10
commit-message:
prefix: "deps"
labels:
- dependencies
# One PR per batch instead of per package. Majors stay separate so a breaking
# change is never buried in a routine batch.
groups:
patch-and-minor:
patterns:
- "*"
update-types:
- minor
- patch
ignore:
# --- Deliberate pins. Removing an ignore here without doing the matching
# migration will produce a PR that builds but breaks at runtime. ---

# Avalonia 12 is a completed-but-parked migration on branch
# upgrade/avalonia-12. Take 11.x servicing, never the major.
- dependency-name: "Avalonia*"
update-types:
- version-update:semver-major

# 5.1.58 is the last build targeting Avalonia 11; 5.1.59+ requires
# Avalonia >= 12 and hard-fails restore with NU1605. This is a PATCH bump,
# so an update-type ignore would not catch it. Drop this entry as part of
# the Avalonia 12 migration.
- dependency-name: "ScottPlot.Avalonia"
versions:
- ">= 5.1.59"

# Pinned to keep the native libs in the [119.0, 120.0) range that managed
# SkiaSharp 3.x requires. A 4.x bump reintroduces GitHub issue #139
# (TypeInitializationException on Linux at startup).
- dependency-name: "SkiaSharp.NativeAssets.*"
update-types:
- version-update:semver-major

# The VSIX targets VS 2022. The 18.x line targets VS 18 and is not
# restorable from nuget.org, so the 17.x line is intentional.
- dependency-name: "Microsoft.VisualStudio.SDK"
update-types:
- version-update:semver-major
- dependency-name: "Microsoft.VSSDK.BuildTools"
update-types:
- version-update:semver-major

# The Velopack library version must move in lockstep with the `vpk` CLI
# pin in .github/workflows/release.yml. A PR bumping only the library
# would desync them and can produce incompatible update packages, so this
# one is handled by hand.
- dependency-name: "Velopack"

- package-ecosystem: github-actions
directory: "/"
schedule:
interval: weekly
day: monday
time: "06:00"
timezone: America/New_York
open-pull-requests-limit: 5
commit-message:
prefix: "ci"
labels:
- dependencies
- github-actions
groups:
actions:
patterns:
- "*"
update-types:
- minor
- patch
9 changes: 7 additions & 2 deletions .github/workflows/claude-code-review.yml
Original file line number Diff line number Diff line change
Expand Up @@ -91,9 +91,14 @@ jobs:
`confirmed: true`) to flag specific lines.
Only post GitHub comments - do not return review text as a message.

# --allowedTools REPLACES the default tool set rather than adding to it.
# Without Read/Grep/Glob the reviewer cannot open a single file in the
# checked-out branch, which is exactly what the prompt above asks it to
# do. PR #399 burned all 20 turns on denied calls
# (permission_denials_count was 10) and failed without posting anything.
claude_args: |
--max-turns 20
--allowedTools "mcp__github_inline_comment__create_inline_comment,Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*)"
--max-turns 40
--allowedTools "Read,Grep,Glob,mcp__github_inline_comment__create_inline_comment,Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*),Bash(git diff:*),Bash(git log:*)"

# Fork PRs: GitHub withholds repository secrets from `pull_request` runs on
# forks and issues a read-only GITHUB_TOKEN, so this workflow cannot review
Expand Down
134 changes: 134 additions & 0 deletions .github/workflows/deploy-planshare.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
name: Deploy PlanShare

# PlanShare is not built by ci.yml (server/** is paths-ignored) and is not in
# PlanViewer.sln, so before this workflow existed a merged PR shipped nothing.
# Production once drifted ~3.5 months behind main, missing two security fixes.
on:
push:
branches: [main]
paths:
- 'server/PlanShare/**'
- '.github/workflows/deploy-planshare.yml'
workflow_dispatch:

permissions:
contents: read

# Never run two deploys at once, and never cancel one mid-flight - an
# interrupted binary swap would leave the service down.
concurrency:
group: deploy-planshare
cancel-in-progress: false

env:
DEPLOY_HOST: stats.erikdarling.com
DEPLOY_USER: deploy
APP_DIR: /opt/planshare
PUBLIC_URL: https://stats.erikdarling.com

jobs:
deploy:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v7

- name: Setup .NET 10.0
uses: actions/setup-dotnet@v6
with:
dotnet-version: 10.0.x

- name: Publish linux-x64 self-contained
run: |
dotnet publish server/PlanShare/PlanShare.csproj \
-c Release -r linux-x64 --self-contained \
-p:PublishSingleFile=true \
-o publish/planshare

- name: Verify build output
run: |
test -f publish/planshare/PlanShare || { echo "::error::binary missing"; exit 1; }
test -f publish/planshare/libe_sqlite3.so || { echo "::error::libe_sqlite3.so missing"; exit 1; }
ls -la publish/planshare/

- name: Configure SSH
env:
DEPLOY_KEY: ${{ secrets.PLANSHARE_DEPLOY_KEY }}
run: |
if [ -z "$DEPLOY_KEY" ]; then
echo "::error::PLANSHARE_DEPLOY_KEY secret is not set - cannot deploy."
exit 1
fi
mkdir -p ~/.ssh && chmod 700 ~/.ssh
printf '%s\n' "$DEPLOY_KEY" > ~/.ssh/deploy_key
chmod 600 ~/.ssh/deploy_key
# Pinned host key: fail closed rather than trusting on first use.
echo 'stats.erikdarling.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICIrnTBiPVE5ulZiFZTBw9/DR/dPVwbAr8ZIvu47/w2J' > ~/.ssh/known_hosts
chmod 600 ~/.ssh/known_hosts

- name: Upload to staging paths
run: |
scp -i ~/.ssh/deploy_key -o BatchMode=yes \
publish/planshare/PlanShare \
"$DEPLOY_USER@$DEPLOY_HOST:$APP_DIR/planshare.new"
scp -i ~/.ssh/deploy_key -o BatchMode=yes \
publish/planshare/libe_sqlite3.so \
"$DEPLOY_USER@$DEPLOY_HOST:$APP_DIR/libe_sqlite3.so.new"

- name: Back up, swap, restart
run: |
ssh -i ~/.ssh/deploy_key -o BatchMode=yes "$DEPLOY_USER@$DEPLOY_HOST" bash -euo pipefail <<'REMOTE'
APP_DIR=/opt/planshare
STAMP=$(date -u +%Y%m%d-%H%M%S)

# Keep one rollback set, plus a DB snapshot. Cheap insurance.
cp -a "$APP_DIR/planshare" "$APP_DIR/planshare.bak-$STAMP"
cp -a "$APP_DIR/libe_sqlite3.so" "$APP_DIR/libe_sqlite3.so.bak-$STAMP"
cp -a "$APP_DIR/data/plans.db" "$APP_DIR/data/plans.db.bak-$STAMP"
echo "$STAMP" > "$APP_DIR/.last-deploy-stamp"

mv "$APP_DIR/planshare.new" "$APP_DIR/planshare"
mv "$APP_DIR/libe_sqlite3.so.new" "$APP_DIR/libe_sqlite3.so"
chmod +x "$APP_DIR/planshare"

sudo /bin/systemctl restart planshare.service
sleep 5
sudo /bin/systemctl is-active planshare.service

# Prune all but the 3 most recent backup sets.
ls -1t "$APP_DIR"/planshare.bak-* 2>/dev/null | tail -n +4 | xargs -r rm -f
ls -1t "$APP_DIR"/libe_sqlite3.so.bak-* 2>/dev/null | tail -n +4 | xargs -r rm -f
ls -1t "$APP_DIR"/data/plans.db.bak-* 2>/dev/null | tail -n +4 | xargs -r rm -f
REMOTE

- name: Verify through nginx
id: verify
run: |
for i in 1 2 3 4 5; do
code=$(curl -s -o /dev/null -w '%{http_code}' -m 20 "$PUBLIC_URL/api/stats" || echo 000)
echo "attempt $i: /api/stats -> $code"
if [ "$code" = "200" ]; then echo "verified=true" >> "$GITHUB_OUTPUT"; exit 0; fi
sleep 5
done
echo "::error::PlanShare did not return 200 through nginx after deploy."
exit 1

- name: Roll back on failed verification
if: failure() && steps.verify.outcome == 'failure'
run: |
echo "::warning::Verification failed - restoring the previous binary."
ssh -i ~/.ssh/deploy_key -o BatchMode=yes "$DEPLOY_USER@$DEPLOY_HOST" bash -euo pipefail <<'REMOTE'
APP_DIR=/opt/planshare
STAMP=$(cat "$APP_DIR/.last-deploy-stamp")
cp -a "$APP_DIR/planshare.bak-$STAMP" "$APP_DIR/planshare"
cp -a "$APP_DIR/libe_sqlite3.so.bak-$STAMP" "$APP_DIR/libe_sqlite3.so"
chmod +x "$APP_DIR/planshare"
sudo /bin/systemctl restart planshare.service
sleep 5
sudo /bin/systemctl is-active planshare.service
echo "Rolled back to $STAMP"
REMOTE

- name: Clean up key
if: always()
run: rm -f ~/.ssh/deploy_key
6 changes: 6 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,12 @@ jobs:
github-artifact-id: '${{ steps.upload-unsigned.outputs.artifact-id }}'
wait-for-completion: true
output-artifact-directory: 'signed/win-x64'
# The action's 600s default means a signing policy that requires manual
# approval has to be approved inside a 10-minute window or the release
# fails partway, leaving a published release with no app assets (this
# happened on v1.17.0). 30 minutes is enough to notice the mail and
# approve. Irrelevant once approval is automatic, but harmless.
wait-for-completion-timeout-in-seconds: 1800

- name: Replace unsigned Windows build with signed
shell: pwsh
Expand Down
40 changes: 40 additions & 0 deletions .signpath/policies/PerformanceStudio/release-signing.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# SignPath GitHub policy for the "release-signing" signing policy.
#
# Path is significant and must match the SignPath project + policy slugs used in
# .github/workflows/release.yml:
# project-slug: PerformanceStudio
# signing-policy-slug: release-signing
#
# This file only takes effect from the repository's DEFAULT branch (main). On any
# other branch it is inert, so changes here do nothing until they are merged to
# main via the usual dev -> main release merge.
#
# What this file does NOT do: it does not enable automatic approval. Approval mode
# lives on the signing policy in the SignPath dashboard. This file adds constraints
# that SignPath enforces on a build before it is willing to sign it.

github-policies:
runners:
# release.yml runs on `windows-latest`. Requiring GitHub-hosted runners means a
# self-hosted runner (which an attacker could register, or which could carry
# dirty state between jobs) can never produce a signed build.
require_github_hosted: true

# Deliberately omitted, with reasons - do not add these without the matching
# repository change, or releases will start failing to sign:
#
# build.disallow_reruns: true
# Blocks signing any build produced by a workflow re-run. A re-run is
# currently the documented recovery path for this repo: `gh release create`
# runs before signing, so a signing failure leaves a published-but-empty
# release that is fixed by re-running (all uploads use --clobber). v1.17.0
# was recovered exactly this way. Enabling this would make that recovery
# impossible. Revisit only if release.yml is restructured so a failed
# signing never publishes anything.
#
# branch_rulesets:
# Requires the protections it lists to actually exist on the branch. `main`
# currently has NO branch protection configured at all, so any ruleset
# requirement here would reject every signing request. Add protection to
# `main` first (e.g. block force pushes, require PRs), then encode the same
# rules here so SignPath verifies they were in force.
2 changes: 1 addition & 1 deletion CITATION.cff
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ authors:
website: "https://erikdarling.com"
repository-code: "https://github.com/erikdarlingdata/PerformanceStudio"
license: MIT
version: "1.17.0"
version: "1.18.0"
date-released: "2026-07-25"
keywords:
- sql-server
Expand Down
2 changes: 1 addition & 1 deletion src/Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
Tests and server/ projects are outside src/ and are unaffected.
-->
<PropertyGroup>
<Version>1.17.0</Version>
<Version>1.18.0</Version>
<Authors>Erik Darling</Authors>
<Company>Darling Data LLC</Company>
<Product>Performance Studio</Product>
Expand Down
Loading
Loading