From 6c34d89a2ffa51ad5443e3d2b852481a87d06114 Mon Sep 17 00:00:00 2001 From: Joe Peavey Date: Thu, 23 Jul 2026 23:43:18 -0400 Subject: [PATCH] Beta channel scaffold --- .gitea/workflows/publish.yml | 77 ++++++++++++ .gitignore | 4 + README.md | 52 ++++++++ docs/BETA-tester-instructions.md | 67 +++++++++++ docs/SETUP-runbook.md | 111 ++++++++++++++++++ docs/update-check-beta.md | 46 ++++++++ docs/versions-beta.json | 1 + .../VisionarySolutions_Suite_Beta.nuspec | 35 ++++++ .../content/.gitkeep | 0 .../description.md | 15 +++ tools/pack.csproj | 15 +++ tools/pack.ps1 | 85 ++++++++++++++ tools/packages.ps1 | 90 ++++++++++++++ tools/publish.ps1 | 49 ++++++++ 14 files changed, 647 insertions(+) create mode 100644 .gitea/workflows/publish.yml create mode 100644 .gitignore create mode 100644 README.md create mode 100644 docs/BETA-tester-instructions.md create mode 100644 docs/SETUP-runbook.md create mode 100644 docs/update-check-beta.md create mode 100644 docs/versions-beta.json create mode 100644 plugins/VisionarySolutions_Suite_Beta/VisionarySolutions_Suite_Beta.nuspec create mode 100644 plugins/VisionarySolutions_Suite_Beta/content/.gitkeep create mode 100644 plugins/VisionarySolutions_Suite_Beta/description.md create mode 100644 tools/pack.csproj create mode 100644 tools/pack.ps1 create mode 100644 tools/packages.ps1 create mode 100644 tools/publish.ps1 diff --git a/.gitea/workflows/publish.yml b/.gitea/workflows/publish.yml new file mode 100644 index 0000000..fbbce6f --- /dev/null +++ b/.gitea/workflows/publish.yml @@ -0,0 +1,77 @@ +name: Publish Q-SYS plugins + +# Packs every plugin under plugins/ and pushes any NEW versions to this +# Gitea instance's NuGet registry. Versions that already exist in the feed +# are skipped, so publishing a revision is just: +# bump in the plugin's .nuspec -> commit -> push to main. +# +# Required repository secret: +# PACKAGE_TOKEN - a Gitea personal access token with package read/write +# scope for the owner account/org. + +# Beta channel: MANUAL publish only, same as production. Run it from +# Actions -> "Publish Q-SYS plugins" -> Run workflow when a beta build is +# ready. (To auto-publish on every commit to a beta build instead, add the +# push: block back - beta is low-stakes, but manual keeps it deliberate.) +on: + workflow_dispatch: + +jobs: + publish: + runs-on: ubuntu-latest + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: Set up .NET SDK + uses: actions/setup-dotnet@v4 + with: + dotnet-version: "8.0.x" + + - name: Install PowerShell + run: dotnet tool install --global PowerShell --version 7.4.6 + + - name: Pack all plugins + run: ~/.dotnet/tools/pwsh ./tools/pack.ps1 -OutputDir dist + + - name: Verify PACKAGE_TOKEN secret + run: | + if [ -z "${{ secrets.PACKAGE_TOKEN }}" ]; then + echo "::error::PACKAGE_TOKEN is EMPTY - the secret is missing, misnamed, or not visible to this repo's workflows" + exit 1 + fi + code=$(curl -s -o /dev/null -w "%{http_code}" \ + -H "Authorization: token ${{ secrets.PACKAGE_TOKEN }}" \ + "${{ github.server_url }}/api/v1/packages/${{ github.repository_owner }}?type=nuget&limit=1") + echo "Token validity check (package API): HTTP $code (200 = valid)" + if [ "$code" != "200" ]; then + echo "::error::PACKAGE_TOKEN is present but Gitea rejects it - regenerate the token" + exit 1 + fi + + - name: Push new versions to Gitea NuGet registry + # Primary: dotnet client with a named basic-auth source (Gitea's + # documented method). Fallback: direct multipart PUT with diagnostics. + run: | + feed="${{ github.server_url }}/api/packages/${{ github.repository_owner }}/nuget" + dotnet nuget add source "$feed/index.json" --name gitea \ + --username "${{ github.actor }}" \ + --password "${{ secrets.PACKAGE_TOKEN }}" \ + --store-password-in-clear-text + if dotnet nuget push "dist/*.nupkg" --source gitea --skip-duplicate; then + echo "Published via dotnet client." + exit 0 + fi + echo "dotnet push failed - trying direct multipart upload..." + fail=0 + for f in dist/*.nupkg; do + code=$(curl -s -o /tmp/push-resp -w "%{http_code}" \ + -u "${{ github.actor }}:${{ secrets.PACKAGE_TOKEN }}" \ + -X PUT -F "package=@$f;type=application/octet-stream" "$feed/") + case "$code" in + 201) echo "PUBLISHED: $f" ;; + 409) echo "skipped (version already in feed): $f" ;; + *) echo "::error::HTTP $code pushing $f"; cat /tmp/push-resp; echo; fail=1 ;; + esac + done + exit $fail diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..401e815 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +dist/ +tools/bin/ +tools/obj/ +*.nupkg diff --git a/README.md b/README.md new file mode 100644 index 0000000..a08ade3 --- /dev/null +++ b/README.md @@ -0,0 +1,52 @@ +# Visionary PacketAV — BETA channel registry + +A parallel copy of the production plugin registry that publishes a **separate +package identity** for pre-release testing. Testers point Q-SYS at a different +URL and get beta builds; everyone else stays on the production feed untouched. + +## What makes this the beta channel + +| | Production | Beta (this repo) | +|---|---|---| +| Package id | `Visionary-PacketAV-Suite` | `Visionary-PacketAV-Suite-Beta` | +| Library entry | Visionary PacketAV Suite | Visionary PacketAV Suite (Beta) | +| Add-package URL | `https://vsplug.in/packetav` | `https://vsplug.in/packetav-beta` | +| In-plugin update feed | `https://vsplug.in/versions` | `https://vsplug.in/versions-beta` | +| Notifications channel | `VSI_PacketAV_Versions` | `VSI_PacketAV_Versions_Beta` | +| Version lineage | its own 3-part counter | its own 3-part counter | + +Both packages live in the **same Gitea org** (`visionary-plugins`), because +Gitea owns NuGet packages at the org level. The distinct package **id** is what +gives Q-SYS a separate My Packages entry — cloning the repo alone would not. + +## Important: never run both channels on one machine + +The beta plugins keep the **same `PluginInfo.Id`** as production so testers +exercise the real blocks. That means Designer will see two files with the same +id if both the prod and beta packages are installed, and one will shadow the +other unpredictably. A tester is on **either** prod **or** beta, never both. +See `docs/BETA-tester-instructions.md`. + +## Publishing a beta build + +1. Drop the beta `.qplugx` (and `icon.png` + any PDFs) into + `plugins/VisionarySolutions_Suite_Beta/content/`. +2. Bump `` in `VisionarySolutions_Suite_Beta.nuspec` (3-part SemVer, + strictly higher than the last beta — **never** a 4-part number). +3. Commit and push to `main`. +4. Actions → **Publish Q-SYS plugins** → **Run workflow** (manual by design). +5. Update the beta update feed: replace `versions-beta.json` in the assets repo + so in-plugin update checks on beta builds see the new version. + +## Promoting a beta to production + +When a beta build passes, the **same `.qplugx` files** move to the production +repo's `content/`, bump the **production** nuspec version, and publish there. +Nothing is rebuilt — the plugin files (and their PluginInfo.Ids) are identical; +only the package wrapper differs. Then update the production `versions.json`. + +## Everything else + +CI, `tools/pack.ps1`, the SemVer rules, the anonymous-read requirement, and the +same `PACKAGE_TOKEN` secret all work exactly as in the production repo — see +that repo's README and `docs/publishing-lessons.md`. diff --git a/docs/BETA-tester-instructions.md b/docs/BETA-tester-instructions.md new file mode 100644 index 0000000..cdc1c11 --- /dev/null +++ b/docs/BETA-tester-instructions.md @@ -0,0 +1,67 @@ +# Joining the Visionary PacketAV Beta + +Thank you for helping test pre-release builds. The beta plugins are the real +blocks that will ship — you're seeing them a cycle early. Setup is the same as +the normal plugins, just a different URL. + +## Before you start — one rule that matters + +**Do not have both the regular and the beta Visionary PacketAV packages +installed on the same computer.** The beta plugins share the same internal IDs +as the production plugins (that's what makes them a true test), so if Q-SYS +Designer finds both, it can silently load the wrong one. Pick one channel per +machine: + +- Testing beta? Remove the regular **Visionary PacketAV** personal package + first (below), then add the beta one. +- Done testing? Do the reverse to go back to production. + +A dedicated test computer is ideal if you have one. + +## Add the beta package + +1. Sign in at `https://library.qsys.com`, then **My Library → My Packages**. +2. If the regular **Visionary PacketAV** entry is present, select it and + **remove** it first (see "Leaving the beta" for why this is safe). +3. Click **Add Personal Package** and enter: + - **Asset name:** `Visionary PacketAV (Beta)` + - **Package URL:** `https://vsplug.in/packetav-beta` +4. Click **Add package**. You'll see the "Personal Package Added" confirmation. + +## Install in Q-SYS Designer + +1. Open **Q-SYS Designer** and sign in to the **same** Library account. +2. **Tools → Show Asset Installer**. +3. On **Available Assets**, find **Visionary-PacketAV-Suite-Beta** and click + **Install**. Wait for **Install Status = Complete**. +4. The plugins appear under **Visionary** in the schematic Plugins list, same + as always. (Tip: check the plugin's **Help** page — beta builds are marked + so you always know which channel you're on.) + +## Getting new beta builds + +When a new beta is published, the Asset Installer shows a higher **Latest +Version** than your **Local Version** — click **Install** to update. If it isn't +showing yet, click **Refresh**; if that's stale, remove and re-add the +`https://vsplug.in/packetav-beta` entry (that forces a fresh read). + +The plugins also tell you in-product: the **Software Update** line on the Help +page checks the beta feed and reports when a newer beta is available. + +## Reporting issues + +Please include: plugin and version (from the Help page footer), Q-SYS Designer +version, Core model, and what you expected vs. what happened. Send to +`support@visionary-av.com` or use the links at `https://vsplug.in/resources`. + +## Leaving the beta / going back to production + +1. **My Packages** → remove the **Visionary PacketAV (Beta)** entry. +2. Add the production package back: **Add Personal Package** → + name `Visionary PacketAV`, URL `https://vsplug.in/packetav`. +3. In **Asset Installer**, install the production suite over the beta. Because + the plugin IDs match, your existing designs keep working — they just move + back to the released version. + +Removing a personal package only detaches the feed; it doesn't touch designs +you've already built. diff --git a/docs/SETUP-runbook.md b/docs/SETUP-runbook.md new file mode 100644 index 0000000..619c030 --- /dev/null +++ b/docs/SETUP-runbook.md @@ -0,0 +1,111 @@ +# Beta channel — one-time setup runbook + +Goal: stand up a beta feed that Q-SYS testers can add without touching the +production feed. You do this once; after that, publishing a beta is just the +five steps in the README. + +Placeholders used below (matching your production setup): +- Gitea host: `3f4dzl.gitea.cloud` +- Org: `visionary-plugins` +- Beta package id: `Visionary-PacketAV-Suite-Beta` + +--- + +## 1. Create the beta repo in Gitea + +Same org as production (packages are owned by the org, so the beta package will +sit alongside prod in the org's Packages list — that's expected). + +1. In Gitea: **`visionary-plugins` org → New Repository**. +2. Name it `qsys-plugin-registry-beta`. Set visibility however you like — + **repo visibility does not affect the feed.** The package feed's anonymous + readability comes from the **org** being Public, which it already is for + prod. (If the org is Public, the beta package is anonymously readable too.) +3. Push this scaffold to it: + + ```bash + cd qsys-plugin-registry-beta + git init -b main + git add . + git commit -m "Beta channel scaffold" + git remote add origin https://3f4dzl.gitea.cloud/visionary-plugins/qsys-plugin-registry-beta.git + git push -u origin main + ``` + +## 2. Add the publish token + +Same token as production works — a Gitea PAT with **package read/write** scope +is org-wide, so it can publish `Visionary-PacketAV-Suite-Beta` too. + +- Beta repo → **Settings → Actions → Secrets** → add **`PACKAGE_TOKEN`** with + the same value you used for the prod repo. +- Confirm a runner is available to the repo (Settings → Actions). + +## 3. Publish a first beta build + +1. Put a beta `.qplugx` set (or just copy the current prod content for a + smoke test) + `icon.png` into + `plugins/VisionarySolutions_Suite_Beta/content/`. +2. Commit and push. +3. Actions → **Publish Q-SYS plugins → Run workflow**. +4. Verify the feed the same way as prod, anonymously: + + ```bash + curl https://3f4dzl.gitea.cloud/api/packages/visionary-plugins/nuget/registration/Visionary-PacketAV-Suite-Beta/index.json + ``` + + You should see JSON listing the beta version(s). + +## 4. Cloudflare short-links (301 redirects) + +Add two aliases next to your existing `packetav` / `versions` rules. Point them +at the beta package's registration URL and the beta versions file. + +| Short link | 301 target | +|---|---| +| `vsplug.in/packetav-beta` | `https://3f4dzl.gitea.cloud/api/packages/visionary-plugins/nuget/registration/Visionary-PacketAV-Suite-Beta/index.json` | +| `vsplug.in/versions-beta` | raw URL of `versions-beta.json` in your assets repo, e.g. `https://3f4dzl.gitea.cloud/visionary-plugins/assets/raw/branch/main/versions-beta.json` | + +Then, in a browser (logged out), open `https://vsplug.in/packetav-beta` and +confirm it lands on the registration JSON — QSC's backend must be able to +follow the redirect, exactly as it does for the prod link. + +## 5. Seed the beta update feed + +Put `versions-beta.json` (in this repo's `docs/`) into the **assets** repo root +so `vsplug.in/versions-beta` resolves. Keep its `package` value equal to the +beta nuspec `` and the `plugins` map equal to the beta build's plugin +versions. Update it every time you publish a new beta. + +## 6. Beta icon (optional but recommended) + +The nuspec references `icon-beta.png` in the assets repo so beta is visually +distinct in Asset Installer. Drop a beta-badged icon there, or change the +`iconUrl` back to the prod `icon.png` if you'd rather not bother. + +--- + +## Version lineage rules (same discipline as prod) + +- Beta `` is **3-part SemVer only** — a 4-part number makes the whole + package vanish from Asset Installer. +- Strictly ascending. The beta counter is **independent** of the prod counter; + don't try to keep them in sync. +- No prerelease `-beta` suffix on the number itself — Q-SYS filters those. The + channel separation is the package id, not a version tag. + +## Promotion flow (beta → production) + +1. Copy the **same** `.qplugx` files from the beta `content/` into the prod + repo's `content/`. +2. Bump the **production** nuspec ``. +3. Commit/push and run the prod publish workflow. +4. Update the prod `versions.json`. + +Because the plugin files (and their PluginInfo.Ids) are byte-identical, this is +a re-wrap, not a rebuild — what testers validated is exactly what ships. + +## Retiring a beta build + +Delete old beta versions from the org's Packages page anytime — beta has no +field-install guarantees, so same-version delete/republish is fine here. diff --git a/docs/update-check-beta.md b/docs/update-check-beta.md new file mode 100644 index 0000000..900c75d --- /dev/null +++ b/docs/update-check-beta.md @@ -0,0 +1,46 @@ +# update_check.lua — beta build changes + +The update-check code is identical between channels except for **two +constants** at the top, plus the per-build `PackageVersion` that already lives +in each `.qplug`. Point beta builds at the beta feed and give them their own +Notifications channel so a beta instance and a prod instance never elect each +other (belt-and-suspenders — testers shouldn't be running both anyway). + +Change these two lines in the beta build's `update_check.lua`: + +```lua +local CH = "VSI_PacketAV_Versions_Beta" -- was: VSI_PacketAV_Versions +local VERSIONS_URL = "https://vsplug.in/versions-beta" -- was: https://vsplug.in/versions +``` + +Everything else — leader election, gateway check, `applyVersions`, Check Now — +is unchanged. + +## PackageVersion + +In each beta `.qplug`, set the `PackageVersion` constant to the **beta** +package's version (the beta nuspec ``), not the prod counter. That's +what the update check compares against `versions-beta.json`'s `package` field to +decide "a newer beta exists." Your `stamp_package_version.py` helper does this; +run it against the beta build folder with the beta version. + +## versions-beta.json shape + +Same schema as prod `versions.json`, hosted at `vsplug.in/versions-beta`: + +```json +{"package":"1.0.0","plugins":{"Visionary~Decoder":"5.2.3","Visionary~Encoder":"5.2.3","Visionary~Router":"5.2.3","Visionary~IR Driver":"5.2.3"}} +``` + +- `package` = beta nuspec `` for the latest beta build. +- `plugins` = each beta plugin's `PluginInfo.Version` (typically ahead of prod). + +`annotate_qplugx.py` already emits a `versions.json` from a build folder; for +beta, publish that output as `versions-beta.json` in the assets repo. + +## Optional: mark the Help page as beta + +Since the plugin IDs match prod, add a small "BETA CHANNEL" tag on the Help page +(and/or set the beta plugins' `PluginInfo.Version` build field distinctly) so a +tester glancing at the schematic always knows which channel is loaded. This is +the same Software Update group from the prod mockup, just with a beta label. diff --git a/docs/versions-beta.json b/docs/versions-beta.json new file mode 100644 index 0000000..54c77b7 --- /dev/null +++ b/docs/versions-beta.json @@ -0,0 +1 @@ +{"package":"1.0.0","plugins":{"Visionary~Decoder":"5.2.3","Visionary~Encoder":"5.2.3","Visionary~Router":"5.2.3","Visionary~IR Driver":"5.2.3"}} diff --git a/plugins/VisionarySolutions_Suite_Beta/VisionarySolutions_Suite_Beta.nuspec b/plugins/VisionarySolutions_Suite_Beta/VisionarySolutions_Suite_Beta.nuspec new file mode 100644 index 0000000..fba3b7a --- /dev/null +++ b/plugins/VisionarySolutions_Suite_Beta/VisionarySolutions_Suite_Beta.nuspec @@ -0,0 +1,35 @@ + + + + + Visionary-PacketAV-Suite-Beta + 1.0.0 + Visionary PacketAV Suite (Beta) + Visionary Solutions Inc. + Visionary Solutions Inc. + false + https://visionary-av.com/products/av-over-ip/ + content/icon.png + https://3f4dzl.gitea.cloud/visionary-plugins/assets/raw/branch/main/icon-beta.png + PLACEHOLDER - replaced from description.md at pack time + BETA test build of the Visionary PacketAV Suite. Pre-release - not for production designs. + Beta channel. See the versions feed and your tester notes for what changed in this build. + 2026 Visionary Solutions + Video Encoder Decoder Beta + + + + + diff --git a/plugins/VisionarySolutions_Suite_Beta/content/.gitkeep b/plugins/VisionarySolutions_Suite_Beta/content/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/plugins/VisionarySolutions_Suite_Beta/description.md b/plugins/VisionarySolutions_Suite_Beta/description.md new file mode 100644 index 0000000..60e9c80 --- /dev/null +++ b/plugins/VisionarySolutions_Suite_Beta/description.md @@ -0,0 +1,15 @@ +BETA TEST BUILD — Visionary PacketAV Suite + +This is a pre-release build for beta testers only. Do not use it in +production designs, and do not install it on the same computer as the +regular Visionary PacketAV Suite package — run one channel at a time. + +A collection of Q-SYS plugins for managing, controlling and monitoring +Encoders and Decoders in Visionary Solutions' PacketAV 4 Series and +5 Series AV-over-IP devices, plus a Router plugin and an IR Driver plugin. + +Q-SYS Designer Software v8.4 or later is required. + +Note: 4 Series and 5 Series are not cross-compatible. + +Beta resources and how to report issues: https://vsplug.in/resources diff --git a/tools/pack.csproj b/tools/pack.csproj new file mode 100644 index 0000000..69f2202 --- /dev/null +++ b/tools/pack.csproj @@ -0,0 +1,15 @@ + + + + netstandard2.0 + false + true + false + false + NU5128;NU5048;NU5125 + + diff --git a/tools/pack.ps1 b/tools/pack.ps1 new file mode 100644 index 0000000..3b43296 --- /dev/null +++ b/tools/pack.ps1 @@ -0,0 +1,85 @@ +<# +.SYNOPSIS + Packs Q-SYS plugin folders into .nupkg files. + +.DESCRIPTION + For each plugin folder under plugins/ (or a single one via -Plugin): + 1. Loads the plugin's .nuspec + 2. Replaces the element with the contents of + description.md, if that file exists next to the nuspec + 3. Packs it into a .nupkg using `dotnet pack` (no Visual Studio needed) + + Output lands in -OutputDir (default: dist/). + +.EXAMPLE + ./tools/pack.ps1 # pack every plugin + ./tools/pack.ps1 -Plugin ExamplePlugin # pack one plugin +#> +[CmdletBinding()] +param( + [string]$Plugin, + [string]$OutputDir = "dist" +) + +$ErrorActionPreference = "Stop" +$repoRoot = Split-Path -Parent $PSScriptRoot +$pluginsDir = Join-Path $repoRoot "plugins" +$packProj = Join-Path $PSScriptRoot "pack.csproj" +$OutputDir = [System.IO.Path]::GetFullPath((Join-Path $repoRoot $OutputDir)) +New-Item -ItemType Directory -Force -Path $OutputDir | Out-Null + +# Which plugin folders to pack? +$targets = if ($Plugin) { + $dir = Join-Path $pluginsDir $Plugin + if (-not (Test-Path $dir)) { throw "Plugin folder not found: $dir" } + Get-Item $dir +} else { + Get-ChildItem $pluginsDir -Directory +} + +$packed = @() +foreach ($dir in $targets) { + $nuspec = Get-ChildItem $dir.FullName -Filter *.nuspec | Select-Object -First 1 + if (-not $nuspec) { + Write-Warning "Skipping $($dir.Name): no .nuspec found" + continue + } + + # Load nuspec and inject description.md if present + [xml]$xml = Get-Content $nuspec.FullName -Raw + $descFile = Join-Path $dir.FullName "description.md" + if (Test-Path $descFile) { + $desc = (Get-Content $descFile -Raw).Trim() + # NuGet caps description at 4000 chars + if ($desc.Length -gt 4000) { $desc = $desc.Substring(0, 4000) } + $node = $xml.package.metadata.GetElementsByTagName("description")[0] + if (-not $node) { + $node = $xml.CreateElement("description", $xml.package.NamespaceURI) + $xml.package.metadata.AppendChild($node) | Out-Null + } + $node.InnerText = $desc # InnerText handles XML escaping + Write-Host " [$($dir.Name)] description injected from description.md" + } + + $id = $xml.package.metadata.id + $version = $xml.package.metadata.version + + # Write the effective nuspec to a temp file (source nuspec stays untouched) + $tmpNuspec = Join-Path ([System.IO.Path]::GetTempPath()) "$id.$version.nuspec" + $xml.Save($tmpNuspec) + + Write-Host " [$($dir.Name)] packing $id $version ..." + dotnet pack $packProj ` + -p:NuspecFile=$tmpNuspec ` + -p:NuspecBasePath=$($dir.FullName) ` + -o $OutputDir --nologo -v quiet + if ($LASTEXITCODE -ne 0) { throw "dotnet pack failed for $($dir.Name)" } + + Remove-Item $tmpNuspec -ErrorAction SilentlyContinue + $pkg = Join-Path $OutputDir "$id.$version.nupkg" + $packed += $pkg + Write-Host " [$($dir.Name)] -> $pkg" -ForegroundColor Green +} + +if ($packed.Count -eq 0) { throw "Nothing was packed." } +Write-Host "`nPacked $($packed.Count) package(s) into $OutputDir" diff --git a/tools/packages.ps1 b/tools/packages.ps1 new file mode 100644 index 0000000..7117028 --- /dev/null +++ b/tools/packages.ps1 @@ -0,0 +1,90 @@ +<# +.SYNOPSIS + List and delete package versions in the Gitea NuGet registry. + +.DESCRIPTION + Listing without -Token uses the anonymous NuGet v3 search endpoint - the + same view Q-SYS Designer gets, so it doubles as a feed health check. + Listing with -Token uses Gitea's management API and adds publish dates. + Deleting always requires -Token (package write scope). + +.EXAMPLE + ./tools/packages.ps1 -List -Owner jodamaster + ./tools/packages.ps1 -List -Owner jodamaster -Id ExamplePlugin -Token $env:GITEA_TOKEN + ./tools/packages.ps1 -Delete -Owner jodamaster -Id ExamplePlugin -Version 1.0.0 -Token $env:GITEA_TOKEN +#> +[CmdletBinding(DefaultParameterSetName = "List")] +param( + [Parameter(ParameterSetName = "List")] [switch]$List, + [Parameter(ParameterSetName = "Delete", Mandatory)] [switch]$Delete, + + [string]$GiteaUrl = "https://3f4dzl.gitea.cloud", + [Parameter(Mandatory)] [string]$Owner, + + [Parameter(ParameterSetName = "List")] + [Parameter(ParameterSetName = "Delete", Mandatory)] + [string]$Id, + + [Parameter(ParameterSetName = "Delete", Mandatory)] [string]$Version, + [string]$Token, # required for -Delete; optional for -List (adds publish dates) + [switch]$Force # skip the delete confirmation prompt +) + +$ErrorActionPreference = "Stop" +$base = $GiteaUrl.TrimEnd('/') +$headers = @{} +if ($Token) { $headers["Authorization"] = "token $Token" } + +# ---------------- Delete ---------------- +if ($PSCmdlet.ParameterSetName -eq "Delete") { + if (-not $Token) { throw "Deleting requires -Token (package write scope)." } + if (-not $Force) { + $answer = Read-Host "Delete $Id $Version from $Owner's registry? This is immediate and permanent. [y/N]" + if ($answer -notmatch '^[Yy]') { Write-Host "Aborted."; exit 0 } + } + $uri = "$base/api/packages/$Owner/nuget/$Id/$Version" + Invoke-RestMethod -Uri $uri -Headers $headers -Method Delete | Out-Null + Write-Host "Deleted $Id $Version." -ForegroundColor Green + Write-Host "Note: Q-SYS expects strictly ascending versions - prefer publishing a higher" + Write-Host "version over deleting and re-using a version number (clients may have cached it)." + exit 0 +} + +# ---------------- List ---------------- +if ($Token) { + # Management API: full info including publish dates (requires sign-in on Gitea Cloud) + $all = @(); $page = 1 + while ($true) { + $uri = "$base/api/v1/packages/$($Owner)?type=nuget&limit=50&page=$page" + $batch = Invoke-RestMethod -Uri $uri -Headers $headers -Method Get + if (-not $batch -or $batch.Count -eq 0) { break } + $all += $batch + if ($batch.Count -lt 50) { break } + $page++ + } + if ($Id) { $all = $all | Where-Object { $_.name -ieq $Id } } + if (-not $all -or $all.Count -eq 0) { Write-Host "No packages found."; exit 0 } + $all | Sort-Object name, created_at | Group-Object name | ForEach-Object { + Write-Host "`n$($_.Name)" -ForegroundColor Cyan + $_.Group | ForEach-Object { + $created = ([datetime]$_.created_at).ToLocalTime().ToString("yyyy-MM-dd HH:mm") + Write-Host (" {0,-20} published {1}" -f $_.version, $created) + } + } +} else { + # Anonymous NuGet v3 search - exactly what Q-SYS Designer sees + $q = if ($Id) { $Id } else { "" } + $uri = "$base/api/packages/$Owner/nuget/query?q=$q&skip=0&take=100" + $result = Invoke-RestMethod -Uri $uri -Method Get + $items = $result.data + if ($Id) { $items = $items | Where-Object { $_.id -ieq $Id } } + if (-not $items -or $items.Count -eq 0) { Write-Host "No packages visible in the anonymous feed for '$Owner'."; exit 0 } + foreach ($pkg in $items) { + Write-Host "`n$($pkg.id) (latest: $($pkg.version))" -ForegroundColor Cyan + foreach ($v in $pkg.versions) { Write-Host " $($v.version)" } + Write-Host " Designer 'Add Personal Package' URL:" -ForegroundColor DarkGray + Write-Host " $base/api/packages/$Owner/nuget/registration/$($pkg.id)/index.json" -ForegroundColor Yellow + } + Write-Host "`n(Anonymous view = what Q-SYS Designer sees. Add -Token for publish dates.)" +} +Write-Host "" diff --git a/tools/publish.ps1 b/tools/publish.ps1 new file mode 100644 index 0000000..da58776 --- /dev/null +++ b/tools/publish.ps1 @@ -0,0 +1,49 @@ +<# +.SYNOPSIS + Packs and pushes Q-SYS plugin packages to a Gitea NuGet registry. + +.DESCRIPTION + Runs pack.ps1, then pushes every .nupkg in dist/ to your Gitea instance. + Already-published versions are skipped (--skip-duplicate), so it is safe + to run repeatedly - only new version numbers actually upload. + +.EXAMPLE + ./tools/publish.ps1 -GiteaUrl https://gitea.example.com -Owner qsys-plugins -Token $env:GITEA_TOKEN + ./tools/publish.ps1 -GiteaUrl https://gitea.example.com -Owner qsys-plugins -Token xxxx -Plugin ExamplePlugin +#> +[CmdletBinding()] +param( + [string]$GiteaUrl = "https://3f4dzl.gitea.cloud", + [Parameter(Mandatory)] [string]$Owner, # Gitea user or org that owns the packages + [Parameter(Mandatory)] [string]$Token, # Gitea personal access token (package write scope) + [string]$Plugin, + [string]$OutputDir = "dist" +) + +$ErrorActionPreference = "Stop" +$repoRoot = Split-Path -Parent $PSScriptRoot + +# 1. Pack +& (Join-Path $PSScriptRoot "pack.ps1") -Plugin $Plugin -OutputDir $OutputDir + +# 2. Push - Gitea needs a NAMED source with basic-auth credentials; +# pushing straight to the URL with --api-key fails (go-gitea/gitea#20717). +$feed = "$($GiteaUrl.TrimEnd('/'))/api/packages/$Owner/nuget/index.json" +$dist = Join-Path $repoRoot $OutputDir +Write-Host "`nPushing to $feed" + +dotnet nuget remove source gitea-publish 2>$null | Out-Null +dotnet nuget add source $feed --name gitea-publish ` + --username publisher --password $Token --store-password-in-clear-text | Out-Null + +try { + Get-ChildItem $dist -Filter *.nupkg | ForEach-Object { + Write-Host " pushing $($_.Name) ..." + dotnet nuget push $_.FullName --source gitea-publish --skip-duplicate + if ($LASTEXITCODE -ne 0) { throw "Push failed for $($_.Name)" } + } +} finally { + dotnet nuget remove source gitea-publish 2>$null | Out-Null +} + +Write-Host "`nDone. Feed URL for Q-SYS Designer users: $feed" -ForegroundColor Green