Skip to content

0.16.0

0.16.0 #10

Workflow file for this run

name: Publish to Scoop
# Renders the Scoop manifest pointing at the official ZIP archives and
# commits it to the configured Scoop bucket repository whenever a non-draft,
# non-prerelease release is published.
permissions:
contents: read
on:
release:
types: [published]
workflow_dispatch:
inputs:
version:
description: 'Release version to publish (must already exist as a GitHub release).'
required: true
type: string
dry_run:
description: 'Render the manifest but do not commit to the bucket repo.'
required: false
type: boolean
default: true
workflow_call:
inputs:
version:
description: 'Release version to publish.'
required: true
type: string
dry_run:
description: 'Render the manifest but do not commit to the bucket repo.'
required: false
type: boolean
default: false
secrets:
SCOOP_BUCKET_PAT:
description: 'PAT (or deploy key token) with write access to the Scoop bucket repository.'
required: false
env:
# Override via the bucket repo's own clone URL when forking.
SCOOP_BUCKET_REPO: mickem/scoop-bucket
SCOOP_BUCKET_BRANCH: main
jobs:
publish:
if: ${{ github.event_name != 'release' || (github.event.release.draft == false && github.event.release.prerelease == false) }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Resolve release tag and version
id: ctx
run: |
if [ "${{ github.event_name }}" = 'release' ]; then
tag='${{ github.event.release.tag_name }}'
version="$tag"
dry_run='false'
# published_at is ISO-8601 like 2026-05-15T14:30:00Z; the
# manifest wants YYYY-MM-DD.
published_at='${{ github.event.release.published_at }}'
release_date="${published_at:0:10}"
else
tag='${{ inputs.version }}'
version='${{ inputs.version }}'
dry_run='${{ inputs.dry_run }}'
release_date=''
fi
if [ -z "$tag" ]; then
echo 'No release tag/version available.' >&2
exit 1
fi
{
echo "tag=$tag"
echo "version=$version"
echo "dry_run=$dry_run"
echo "release_date=$release_date"
} >> "$GITHUB_OUTPUT"
- uses: actions/setup-python@v6
with:
python-version: '3.11'
- name: Render Scoop manifest
run: |
render_args=(
--version "${{ steps.ctx.outputs.version }}"
--release-tag "${{ steps.ctx.outputs.tag }}"
--templates packaging/scoop
--output dist/scoop
--download-dir dist/_assets
--asset "ZIP_X64=NSCP-${{ steps.ctx.outputs.version }}-x64.zip"
--asset "ZIP_X86=NSCP-${{ steps.ctx.outputs.version }}-Win32.zip"
)
if [ -n "${{ steps.ctx.outputs.release_date }}" ]; then
render_args+=(--release-date "${{ steps.ctx.outputs.release_date }}")
fi
python packaging/scripts/render_templates.py "${render_args[@]}"
- name: Validate manifest is parseable JSON
run: python -m json.tool dist/scoop/nsclient.json > /dev/null
- name: Verify rendered hashes round-trip from URL
run: |
python packaging/scripts/verify_manifest_hashes.py scoop dist/scoop
- name: Verify Scoop bin paths exist in the ZIP
run: |
set -euo pipefail
python <<'PY'
import json, sys, zipfile
from pathlib import Path
manifest = json.loads(Path("dist/scoop/nsclient.json").read_text())
version = "${{ steps.ctx.outputs.version }}"
archives = {
"x64": Path(f"dist/_assets/NSCP-{version}-x64.zip"),
"Win32": Path(f"dist/_assets/NSCP-{version}-Win32.zip"),
}
# Normalise manifest bin entries to the path string scoop uses to
# locate the file inside the extracted archive.
bins = []
for entry in manifest.get("bin", []):
if isinstance(entry, str):
bins.append(entry)
elif isinstance(entry, list) and entry:
bins.append(entry[0])
missing = []
for arch, zpath in archives.items():
with zipfile.ZipFile(zpath) as zf:
names = {n.replace("\\", "/") for n in zf.namelist()}
for b in bins:
target = b.replace("\\", "/")
if target not in names:
missing.append(f"{zpath.name}: {b}")
if missing:
for m in missing:
print(f"::error::scoop bin entry not found — {m}")
sys.exit(1)
print("All scoop bin entries verified in both archives.")
PY
- name: Upload rendered manifest
uses: actions/upload-artifact@v6
with:
name: scoop-manifest-${{ steps.ctx.outputs.version }}
path: dist/scoop/nsclient.json
if-no-files-found: error
- name: Commit manifest to Scoop bucket repo
if: ${{ steps.ctx.outputs.dry_run != 'true' }}
env:
SCOOP_BUCKET_PAT: ${{ secrets.SCOOP_BUCKET_PAT }}
run: |
set -euo pipefail
if [ -z "${SCOOP_BUCKET_PAT:-}" ]; then
echo "::warning::SCOOP_BUCKET_PAT secret is not configured. Skipping commit."
exit 0
fi
# Pass the PAT via http.extraHeader so it never lands in argv or
# the cloned repo's .git/config. GitHub Actions masks the secret
# in logs but cannot mask process state on the runner.
auth_header="Authorization: Basic $(printf 'x-access-token:%s' "$SCOOP_BUCKET_PAT" | base64 -w0)"
bucket_dir="$(mktemp -d)"
git -c http.extraHeader="$auth_header" clone --depth=1 \
--branch "$SCOOP_BUCKET_BRANCH" \
"https://github.com/${SCOOP_BUCKET_REPO}.git" \
"$bucket_dir"
mkdir -p "$bucket_dir/bucket"
cp dist/scoop/nsclient.json "$bucket_dir/bucket/nsclient.json"
cd "$bucket_dir"
git config user.name 'github-actions[bot]'
git config user.email 'github-actions[bot]@users.noreply.github.com'
git add bucket/nsclient.json
if git diff --cached --quiet; then
echo 'No changes to nsclient.json; nothing to commit.'
exit 0
fi
git commit -m "nsclient: bump to ${{ steps.ctx.outputs.version }}"
git -c http.extraHeader="$auth_header" push \
origin "HEAD:${SCOOP_BUCKET_BRANCH}"