Beta channel scaffold

This commit is contained in:
Joe Peavey
2026-07-23 23:43:18 -04:00
commit 6c34d89a2f
14 changed files with 647 additions and 0 deletions
+77
View File
@@ -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 <version> 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
+4
View File
@@ -0,0 +1,4 @@
dist/
tools/bin/
tools/obj/
*.nupkg
+52
View File
@@ -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 `<version>` 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`.
+67
View File
@@ -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.
+111
View File
@@ -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 `<version>` 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 `<version>` 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 `<version>`.
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.
+46
View File
@@ -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 `<version>`), 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 `<version>` 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.
+1
View File
@@ -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"}}
@@ -0,0 +1,35 @@
<?xml version='1.0' encoding='utf-8'?>
<!--
BETA CHANNEL package manifest.
Separate package identity from production so Q-SYS shows a distinct
"My Packages" entry. The plugin files inside keep their real
PluginInfo.Id values, so testers exercise the actual blocks - which is
why a machine must NOT have both the prod and beta packages installed
at once (same-Id files shadow each other in Designer).
Version is a 3-part SemVer delivery counter for the BETA lineage only,
independent of the production package's counter. Q-SYS offers the
highest version at the registration URL, so just bump this each beta.
The <description> element is replaced from description.md at pack time.
-->
<package xmlns="http://schemas.microsoft.com/packaging/2011/08/nuspec.xsd">
<metadata>
<id>Visionary-PacketAV-Suite-Beta</id>
<version>1.0.0</version>
<title>Visionary PacketAV Suite (Beta)</title>
<authors>Visionary Solutions Inc.</authors>
<owners>Visionary Solutions Inc.</owners>
<requireLicenseAcceptance>false</requireLicenseAcceptance>
<projectUrl>https://visionary-av.com/products/av-over-ip/</projectUrl>
<icon>content/icon.png</icon>
<iconUrl>https://3f4dzl.gitea.cloud/visionary-plugins/assets/raw/branch/main/icon-beta.png</iconUrl>
<description>PLACEHOLDER - replaced from description.md at pack time</description>
<summary>BETA test build of the Visionary PacketAV Suite. Pre-release - not for production designs.</summary>
<releaseNotes>Beta channel. See the versions feed and your tester notes for what changed in this build.</releaseNotes>
<copyright>2026 Visionary Solutions</copyright>
<tags>Video Encoder Decoder Beta</tags>
</metadata>
<files>
<file src="content/**" target="content" />
</files>
</package>
@@ -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
+15
View File
@@ -0,0 +1,15 @@
<!--
Helper project so `dotnet pack` can build a .nupkg straight from a .nuspec.
It produces no assemblies; all package contents come from the nuspec passed
in via -p:NuspecFile / -p:NuspecBasePath (see pack.ps1).
-->
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<IncludeBuildOutput>false</IncludeBuildOutput>
<SuppressDependenciesWhenPacking>true</SuppressDependenciesWhenPacking>
<EnableDefaultItems>false</EnableDefaultItems>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<NoWarn>NU5128;NU5048;NU5125</NoWarn>
</PropertyGroup>
</Project>
+85
View File
@@ -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 <description> 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"
+90
View File
@@ -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 ""
+49
View File
@@ -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