Skip to content

Commit ed2df48

Browse files
committed
Initial commit
1 parent 6e6c4ae commit ed2df48

50 files changed

Lines changed: 4604 additions & 0 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/release.yml

Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
1+
name: release
2+
3+
on:
4+
push:
5+
branches: [main, prerelease]
6+
paths:
7+
- 'Huginn/PackageVersion.txt'
8+
workflow_dispatch:
9+
10+
permissions:
11+
contents: write
12+
13+
jobs:
14+
validate-version:
15+
runs-on: ubuntu-latest
16+
outputs:
17+
version: ${{ steps.read.outputs.value }}
18+
isPrerelease: ${{ steps.read.outputs.prerelease }}
19+
steps:
20+
- uses: actions/checkout@v6
21+
22+
- name: Read version and validate against branch
23+
id: read
24+
shell: bash
25+
run: |
26+
VERSION=$(cat Huginn/PackageVersion.txt | tr -d '\r' | tr -d '\n' | xargs)
27+
echo "Version: $VERSION"
28+
29+
if [[ "$VERSION" == *"-"* ]]; then
30+
HAS_SUFFIX=true
31+
else
32+
HAS_SUFFIX=false
33+
fi
34+
35+
BRANCH="${GITHUB_REF#refs/heads/}"
36+
if [ "$BRANCH" = "main" ] && [ "$HAS_SUFFIX" = "true" ]; then
37+
echo "ERROR: main branch must not have a prerelease suffix (got '$VERSION')"
38+
exit 1
39+
fi
40+
if [ "$BRANCH" = "prerelease" ] && [ "$HAS_SUFFIX" = "false" ]; then
41+
echo "ERROR: prerelease branch requires a suffix (got '$VERSION')"
42+
exit 1
43+
fi
44+
45+
echo "value=$VERSION" >> "$GITHUB_OUTPUT"
46+
echo "prerelease=$HAS_SUFFIX" >> "$GITHUB_OUTPUT"
47+
48+
pack:
49+
needs: validate-version
50+
name: Pack ${{ matrix.rid }}
51+
runs-on: ${{ matrix.os }}
52+
strategy:
53+
fail-fast: false
54+
matrix:
55+
include:
56+
- os: windows-latest
57+
rid: win-x64
58+
framework: net10.0-windows10.0.17763.0
59+
mainExe: Huginn.exe
60+
iconPath: Huginn/Assets/Huginn.ico
61+
extraVpkArgs: ''
62+
- os: macos-latest
63+
rid: osx-arm64
64+
framework: net10.0
65+
mainExe: Huginn
66+
iconPath: icon.icns
67+
extraVpkArgs: --bundleId com.malforge.huginn --channel osx-arm64
68+
- os: macos-latest
69+
rid: osx-x64
70+
framework: net10.0
71+
mainExe: Huginn
72+
iconPath: icon.icns
73+
extraVpkArgs: --bundleId com.malforge.huginn --channel osx-x64
74+
75+
steps:
76+
- uses: actions/checkout@v6
77+
78+
- uses: actions/setup-dotnet@v5
79+
with:
80+
dotnet-version: '10.0.x'
81+
82+
- name: Install Velopack CLI
83+
shell: bash
84+
run: dotnet tool install --global vpk
85+
86+
# macOS bundles want an .icns file. Build an iconset directory from the
87+
# PNG and use iconutil to produce the icns — works at any source size.
88+
# When the icon gets redesigned, drop a higher-resolution PNG into
89+
# Huginn/Assets/ (1024x1024 ideal) and this step produces a crisp icns.
90+
# `sips` and `iconutil` are preinstalled on macOS runners.
91+
- name: Generate macOS .icns from PNG
92+
if: runner.os == 'macOS'
93+
shell: bash
94+
run: |
95+
set -e
96+
src=Huginn/Assets/Huginn.png
97+
mkdir -p icon.iconset
98+
sips -z 16 16 "$src" --out icon.iconset/icon_16x16.png
99+
sips -z 32 32 "$src" --out icon.iconset/icon_16x16@2x.png
100+
sips -z 32 32 "$src" --out icon.iconset/icon_32x32.png
101+
sips -z 64 64 "$src" --out icon.iconset/icon_32x32@2x.png
102+
sips -z 128 128 "$src" --out icon.iconset/icon_128x128.png
103+
sips -z 256 256 "$src" --out icon.iconset/icon_128x128@2x.png
104+
sips -z 256 256 "$src" --out icon.iconset/icon_256x256.png
105+
sips -z 512 512 "$src" --out icon.iconset/icon_256x256@2x.png
106+
sips -z 512 512 "$src" --out icon.iconset/icon_512x512.png
107+
sips -z 1024 1024 "$src" --out icon.iconset/icon_512x512@2x.png
108+
iconutil -c icns icon.iconset -o icon.icns
109+
110+
- name: Publish
111+
shell: bash
112+
run: |
113+
dotnet publish Huginn/Huginn.csproj \
114+
-c Release \
115+
-f ${{ matrix.framework }} \
116+
-r ${{ matrix.rid }} \
117+
--self-contained \
118+
-p:PublishSingleFile=true \
119+
-o publish
120+
121+
- name: vpk pack
122+
shell: bash
123+
run: |
124+
vpk pack \
125+
--packId Huginn \
126+
--packVersion ${{ needs.validate-version.outputs.version }} \
127+
--packDir publish \
128+
--mainExe ${{ matrix.mainExe }} \
129+
--packTitle "Huginn" \
130+
--packAuthors "Morten Aune Lyrstad" \
131+
--runtime ${{ matrix.rid }} \
132+
--icon ${{ matrix.iconPath }} \
133+
${{ matrix.extraVpkArgs }}
134+
135+
- name: Upload pack artifacts
136+
uses: actions/upload-artifact@v7
137+
with:
138+
name: pack-${{ matrix.rid }}
139+
if-no-files-found: error
140+
retention-days: 7
141+
path: Releases/*
142+
143+
release:
144+
needs: [validate-version, pack]
145+
runs-on: ubuntu-latest
146+
permissions:
147+
contents: write
148+
steps:
149+
- uses: actions/checkout@v6
150+
151+
- name: Compose release body
152+
id: notes
153+
shell: bash
154+
run: |
155+
VERSION="${{ needs.validate-version.outputs.version }}"
156+
IS_PRERELEASE="${{ needs.validate-version.outputs.isPrerelease }}"
157+
158+
# State-machine extraction: print lines after the "v.<VERSION>" header
159+
# and before the next "v.<anything>" header. Tolerant of CRLF endings.
160+
# Avoids awk's range-pattern gotcha (range collapses to a single line
161+
# when both endpoint patterns happen to match the same line, which
162+
# happens when ReleaseNotes.txt only has one version entry).
163+
NOTES=$(awk -v ver="$VERSION" '
164+
{ sub(/\r$/, "") }
165+
$0 == "v." ver { found=1; next }
166+
found && /^v\./ { exit }
167+
found { print }
168+
' Huginn/ReleaseNotes.txt)
169+
170+
echo "::group::Extracted release notes for v$VERSION"
171+
echo "$NOTES"
172+
echo "::endgroup::"
173+
174+
if [ -z "$NOTES" ]; then
175+
NOTES="No release notes provided for v$VERSION."
176+
fi
177+
178+
INSTALL_GUIDE=$(cat <<'EOF'
179+
## First time installing?
180+
181+
Huginn is an open-source personal project and isn't code-signed. Windows SmartScreen and macOS Gatekeeper will both show a warning on first install — **that's expected**. Code-signing certificates cost real money and aren't worth it for a free personal tool. The source is public on this repo; you can read it.
182+
183+
**Windows**
184+
185+
1. Download `Huginn-win-Setup.exe` below.
186+
2. Run it. SmartScreen will warn that the app is from an unknown publisher.
187+
3. Click **More info**, then **Run anyway**.
188+
189+
**macOS**
190+
191+
1. Download the `.pkg` for your Mac: `Huginn-osx-arm64-Setup.pkg` (Apple Silicon — M1/M2/M3/M4) or `Huginn-osx-x64-Setup.pkg` (Intel). Not sure? Apple menu → About This Mac → the **Chip**/**Processor** line.
192+
2. Open it. macOS will block it because it isn't signed — that's expected. In the warning, click **Done** (or **Cancel**) — **not "Move to Trash"**, which deletes the installer you just downloaded.
193+
3. Open **System Settings → Privacy & Security**, scroll to the **Security** section, and click **Open Anyway** next to the note about Huginn being blocked. Authenticate, confirm **Open**, then follow the installer.
194+
195+
Once installed, future updates download and apply themselves the next time you launch the app — you don't need to repeat any of this.
196+
197+
---
198+
EOF
199+
)
200+
201+
PRERELEASE_WARNING=""
202+
if [ "$IS_PRERELEASE" = "true" ]; then
203+
PRERELEASE_WARNING=$'\n> **Pre-release notice:** this is a pre-release build intended for testing. It may contain bugs or incomplete features. Please report anything weird.\n'
204+
fi
205+
206+
BODY="${INSTALL_GUIDE}${PRERELEASE_WARNING}
207+
208+
## What's new
209+
210+
${NOTES}"
211+
212+
{
213+
echo "value<<EOF"
214+
echo "$BODY"
215+
echo "EOF"
216+
} >> "$GITHUB_OUTPUT"
217+
218+
- name: Download all pack artifacts
219+
uses: actions/download-artifact@v8
220+
with:
221+
path: releases
222+
pattern: pack-*
223+
merge-multiple: true
224+
225+
- name: Create GitHub Release
226+
uses: softprops/action-gh-release@v3
227+
env:
228+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
229+
with:
230+
tag_name: v${{ needs.validate-version.outputs.version }}
231+
name: Huginn v${{ needs.validate-version.outputs.version }}
232+
body: ${{ steps.notes.outputs.value }}
233+
prerelease: ${{ needs.validate-version.outputs.isPrerelease == 'true' }}
234+
target_commitish: ${{ github.sha }}
235+
files: releases/*
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
name: version-guard
2+
3+
on:
4+
pull_request:
5+
types: [opened, synchronize, reopened]
6+
paths:
7+
- 'Huginn/**'
8+
9+
jobs:
10+
check-version:
11+
runs-on: ubuntu-latest
12+
name: Validate version + release notes
13+
14+
steps:
15+
- uses: actions/checkout@v6
16+
with:
17+
fetch-depth: 0
18+
19+
- name: Get base SHA
20+
run: |
21+
git fetch origin ${{ github.base_ref }}
22+
echo "BASE_SHA=$(git rev-parse origin/${{ github.base_ref }})" >> "$GITHUB_ENV"
23+
24+
- name: Check version + release-notes bumps
25+
shell: pwsh
26+
run: |
27+
$baseSha = $env:BASE_SHA
28+
$headSha = "${{ github.sha }}"
29+
Write-Host "Comparing $baseSha...$headSha"
30+
31+
$changedFiles = git diff --name-only $baseSha $headSha
32+
33+
$packageFolders = Get-ChildItem -Path "." -Filter "PackageVersion.txt" -Recurse |
34+
ForEach-Object { $_.DirectoryName }
35+
36+
$errors = @()
37+
38+
foreach ($folder in $packageFolders) {
39+
$relativePath = $folder -replace [regex]::Escape($PWD.Path + [IO.Path]::DirectorySeparatorChar), ""
40+
$relativePath = $relativePath -replace '\\', '/'
41+
42+
Write-Host "`nChecking folder: $relativePath"
43+
44+
$folderChanges = $changedFiles | Where-Object {
45+
$_ -like "$relativePath/*" -and
46+
$_ -notlike "*/PackageVersion.txt" -and
47+
$_ -notlike "*/ReleaseNotes.txt"
48+
}
49+
50+
if (-not $folderChanges) {
51+
Write-Host " No code changes in this folder, skipping"
52+
continue
53+
}
54+
55+
Write-Host " Found code changes:"
56+
$folderChanges | ForEach-Object { Write-Host " - $_" }
57+
58+
$versionFile = "$relativePath/PackageVersion.txt"
59+
$versionChanged = $changedFiles -contains $versionFile
60+
61+
if (-not $versionChanged) {
62+
$errors += "Files changed in $relativePath but PackageVersion.txt was not updated"
63+
continue
64+
}
65+
Write-Host " PackageVersion.txt was updated"
66+
67+
$newVersion = (Get-Content "$folder/PackageVersion.txt").Trim()
68+
Write-Host " New version: $newVersion"
69+
70+
$releaseNotesFile = Join-Path $folder "ReleaseNotes.txt"
71+
if (Test-Path $releaseNotesFile) {
72+
$notesChanged = $changedFiles -contains "$relativePath/ReleaseNotes.txt"
73+
if (-not $notesChanged) {
74+
$errors += "PackageVersion.txt changed in $relativePath but ReleaseNotes.txt was not updated"
75+
continue
76+
}
77+
78+
$notes = Get-Content $releaseNotesFile -Raw
79+
if ($notes -notmatch "v\.$([regex]::Escape($newVersion))") {
80+
$errors += "Version $newVersion not found in ReleaseNotes.txt for $relativePath"
81+
continue
82+
}
83+
Write-Host " ReleaseNotes.txt updated with v.$newVersion"
84+
}
85+
else {
86+
Write-Host " (no ReleaseNotes.txt in this folder)"
87+
}
88+
}
89+
90+
if ($errors.Count -gt 0) {
91+
Write-Host "`nVERSION GUARD FAILURES:" -ForegroundColor Red
92+
$errors | ForEach-Object { Write-Host " - $_" -ForegroundColor Red }
93+
Write-Host "`nWhen you change code in a folder with PackageVersion.txt:"
94+
Write-Host " 1. Bump PackageVersion.txt"
95+
Write-Host " 2. Add a 'v.<newVersion>' section at the top of ReleaseNotes.txt"
96+
exit 1
97+
}
98+
99+
Write-Host "`nAll version checks passed."

.gitignore

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# Build output
2+
bin/
3+
obj/
4+
publish/
5+
out/
6+
deploy/
7+
8+
# Visual Studio
9+
.vs/
10+
*.user
11+
*.suo
12+
*.userosscache
13+
*.sln.docstates
14+
*.vsidx
15+
16+
# Rider / JetBrains
17+
.idea/
18+
*.iml
19+
20+
# ReSharper
21+
_ReSharper*/
22+
*.[Rr]e[Ss]harper
23+
*.DotSettings.user
24+
25+
# OS
26+
.DS_Store
27+
Thumbs.db
28+
desktop.ini
29+
30+
# Logs and temp
31+
*.log
32+
*.tmp
33+
34+
# NuGet
35+
*.nupkg
36+
.nuget/
37+
38+
# Local state
39+
*.local.json
40+
41+
# Claude Code (per-machine permissions, never commit)
42+
.claude/settings.local.json
43+
44+
# Velopack output (release artifacts live in GitHub Releases, not the repo)
45+
Releases/

0 commit comments

Comments
 (0)