mirror of
https://github.com/screentinker/screentinker.git
synced 2026-08-14 22:33:12 -06:00
Some checks are pending
CI / Unit tests (node --test) (push) Waiting to run
CI / OpenAPI spec lint (push) Waiting to run
CI / Android unit tests (Kotlin schedule evaluator vectors) (push) Waiting to run
CI / Licence gate + SBOM (production deps) (push) Waiting to run
CI / Boot smoke + version check (push) Waiting to run
The licence audit that found org.json in the APK was run by hand. Nothing stopped the next
transitive dependency arriving the same way, and "we track licences" was a claim rather than
something anyone could check.
TWO GATES, BOTH FAIL CLOSED.
scripts/license-check.js audits the server's npm tree. scripts/android-license-check.js
resolves the real releaseRuntimeClasspath — everything that can enter the APK a customer
installs — and checks it against android/licenses.json, where each entry records the licence
AND the evidence for it. A dependency nobody has recorded fails the build. That is the case
worth catching: org.json reached customers because it arrived transitively and nothing ever
asked what licence it carried.
Denied: AGPL, GPL, SSPL, Commons Clause, BUSL, and the JSON Licence. Weak copyleft (LGPL,
MPL, EPL, CDDL) is reported but does not fail — it is a judgement, and the judgement should
be made by someone who knows they are making it. Anything unrecognised fails; a package whose
licence we cannot identify is not one we ship.
⚠️ THE SERVER GATE INSTALLS --omit=dev, AND THAT IS THE POINT. A developer checkout carries
sharp, whose @img/sharp-wasm32 declares LGPL-3.0-or-later. It is a test fixture generator that
never reaches a server, but a scanner pointed at a dev tree reports LGPL and contradicts the
answer we give customers. Auditing the production install is what makes the answer defensible.
SBOM. Every release now publishes screentinker-sbom-<version>.cdx.json — CycloneDX 1.5, every
production dependency with version, purl and licence, generated from a production install. CI
uploads one on every run too. That is what turns the claim into something a customer or an
underwriter can verify themselves.
Neither script takes a dependency: a gate that needs its own supply chain audited is worth
less than one that does not.
Verified by mutation rather than assumed. Injecting GPL-3.0-or-later, AGPL-3.0, the JSON
Licence, SSPL-1.0, and a package with no licence field each fail the server gate; MIT and
LGPL pass (LGPL reported). Removing the org.json exclusion fails the Android gate by name;
dropping a group from the policy fails it as unrecorded. Both restored, both green.
Found and fixed while building it: npm ls exits non-zero for any tree problem — an extraneous
package is enough — which made the gate abort instead of auditing. It now reads the listing
either way and only aborts on genuinely empty output.
docs/licensing.md records the policy, how to run the gates, and the dev-vs-production trap.
1676/1676 pass.
249 lines
11 KiB
YAML
249 lines
11 KiB
YAML
name: Release
|
|
|
|
# Fires when a version tag is pushed (e.g. v1.8.0). Builds + publishes artifacts
|
|
# only - nothing here deploys to production.
|
|
on:
|
|
push:
|
|
tags: ['v*']
|
|
|
|
permissions:
|
|
contents: write # create the GitHub Release
|
|
packages: write # push the image to ghcr.io
|
|
|
|
concurrency:
|
|
group: release-${{ github.ref }}
|
|
cancel-in-progress: false # never cancel a release mid-publish
|
|
|
|
jobs:
|
|
# Fail-fast: a hand-pushed tag that disagrees with VERSION must not publish
|
|
# anything (the artifacts would report the wrong version). Gates everything.
|
|
verify:
|
|
name: Verify tag matches VERSION
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v6
|
|
- name: Assert pushed tag equals VERSION
|
|
run: |
|
|
TAG="${GITHUB_REF_NAME#v}"
|
|
FILE="$(cat VERSION)"
|
|
echo "pushed tag: ${GITHUB_REF_NAME} (stripped: $TAG) VERSION file: $FILE"
|
|
if [ "$TAG" != "$FILE" ]; then
|
|
echo "::error::Tag ${GITHUB_REF_NAME} does not match VERSION ($FILE) - refusing to publish."
|
|
exit 1
|
|
fi
|
|
echo "OK: tag matches VERSION ($FILE)"
|
|
|
|
test:
|
|
name: Tests
|
|
needs: verify
|
|
runs-on: ubuntu-latest
|
|
defaults:
|
|
run:
|
|
working-directory: server
|
|
steps:
|
|
- uses: actions/checkout@v6
|
|
- uses: actions/setup-node@v6
|
|
with:
|
|
node-version: '20'
|
|
cache: npm
|
|
cache-dependency-path: server/package-lock.json
|
|
- run: npm ci
|
|
- run: npm test
|
|
|
|
artifacts:
|
|
name: Tarball + Tizen .wgt + GitHub Release
|
|
needs: test
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v6
|
|
with:
|
|
fetch-depth: 0 # full history, for release notes
|
|
|
|
- name: Resolve version + previous tag
|
|
id: ver
|
|
run: |
|
|
VERSION="$(cat VERSION)"
|
|
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
|
|
echo "tag=${GITHUB_REF_NAME}" >> "$GITHUB_OUTPUT"
|
|
PREV="$(git describe --tags --abbrev=0 "${GITHUB_REF_NAME}^" 2>/dev/null || true)"
|
|
echo "prev=$PREV" >> "$GITHUB_OUTPUT"
|
|
# #80: a version carrying a -suffix (e.g. 1.9.0-rc1) is a pre-release.
|
|
case "$VERSION" in *-*) PRE=true ;; *) PRE=false ;; esac
|
|
echo "prerelease=$PRE" >> "$GITHUB_OUTPUT"
|
|
echo "Releasing ${GITHUB_REF_NAME} (version $VERSION, prerelease=$PRE); previous tag: ${PREV:-<none>}"
|
|
|
|
- name: Build Tizen .wgt (unsigned in CI)
|
|
run: |
|
|
chmod +x tizen/build-wgt.sh
|
|
( cd tizen && ./build-wgt.sh ) # no Tizen CLI on the runner => unsigned zip
|
|
cp tizen/ScreenTinker.wgt ScreenTinker.wgt
|
|
ls -la ScreenTinker.wgt
|
|
|
|
- name: Build BrightSign autorun.zip (single-file player installer)
|
|
run: |
|
|
chmod +x scripts/build-autorun-zip.sh
|
|
./scripts/build-autorun-zip.sh -o autorun.zip
|
|
ls -la autorun.zip
|
|
|
|
# A published SBOM is what turns "we track licences" into something a customer or an
|
|
# underwriter can check for themselves. Built from a PRODUCTION install — a dev tree
|
|
# would list packages (sharp and its LGPL-bearing wasm variant) that never ship.
|
|
- name: Generate SBOM (production dependencies)
|
|
run: |
|
|
( cd server && npm ci --omit=dev )
|
|
node scripts/license-check.js --sbom "screentinker-sbom-${{ steps.ver.outputs.version }}.cdx.json"
|
|
ls -la screentinker-sbom-*.cdx.json
|
|
|
|
- name: Build source tarball (bundles the .wgt; the signed apk is added by scripts/finalize-release.sh)
|
|
run: |
|
|
OUT="screentinker-${{ steps.ver.outputs.version }}.tar.gz"
|
|
tar czf "$OUT" \
|
|
--exclude='node_modules' --exclude='.git' --exclude='.github' \
|
|
--exclude='*.db' --exclude='*.db-wal' --exclude='*.db-shm' --exclude='*.db.*' \
|
|
--exclude='server/uploads' --exclude='server/certs' --exclude='server/test' \
|
|
--exclude='*.apk' \
|
|
server frontend scripts docs VERSION README.md LICENSE .env.example ScreenTinker.wgt brightsign
|
|
echo "TARBALL=$OUT" >> "$GITHUB_ENV"
|
|
ls -la "$OUT"
|
|
|
|
- name: Generate release notes
|
|
run: |
|
|
PREV="${{ steps.ver.outputs.prev }}"
|
|
VERSION="${{ steps.ver.outputs.version }}"
|
|
|
|
# Prefer the hand-written CHANGELOG section for this version.
|
|
#
|
|
# The generated list is commit SUBJECTS, which describe the work, not the release: cutting
|
|
# 1.9.34 produced a page reading "chore(release): v1.9.34" and one changelog commit, while
|
|
# the entry describing single sign-on, the native-dependency removal, the update fixes and
|
|
# every outside contributor sat in CHANGELOG.md and was never published. The notes on the
|
|
# release page are what most people actually read, so they should be the written ones.
|
|
#
|
|
# awk rather than sed: the body contains regex metacharacters and markdown that a sed range
|
|
# would mangle. This takes everything between `## <version>` and the next `## ` heading.
|
|
CHANGELOG_BODY="$(awk -v v="## $VERSION" '
|
|
$0 == v {found=1; next}
|
|
found && /^## / {exit}
|
|
found {print}
|
|
' CHANGELOG.md)"
|
|
|
|
{
|
|
echo "## ScreenTinker ${{ steps.ver.outputs.tag }}"
|
|
echo
|
|
if [ -n "$(printf '%s' "$CHANGELOG_BODY" | tr -d '[:space:]')" ]; then
|
|
echo "$CHANGELOG_BODY"
|
|
else
|
|
# No entry for this version — fall back to commit subjects rather than publish a
|
|
# release with no notes at all. scripts/bump-version.sh already warns when the
|
|
# CHANGELOG has no matching heading; this is the same gap showing up downstream.
|
|
echo "_No CHANGELOG entry for $VERSION; listing commits instead._"
|
|
echo
|
|
echo "### Changes"
|
|
if [ -n "$PREV" ]; then
|
|
git log --no-merges --pretty='- %s' "${PREV}..${{ steps.ver.outputs.tag }}"
|
|
else
|
|
echo "_First tagged release. Most recent changes:_"
|
|
git log --no-merges --pretty='- %s' -n 30 "${{ steps.ver.outputs.tag }}"
|
|
fi
|
|
fi
|
|
echo
|
|
echo "### Artifacts"
|
|
echo "- \`${TARBALL}\` - bundle: server + frontend source + the Tizen .wgt (the signed Android APK is added at the root during release finalization)."
|
|
echo "- \`ScreenTinker.wgt\` - Tizen TV web app, **unsigned - for inspection only**."
|
|
echo " Sign it with your own Samsung certificate (Tizen Studio + a profile that includes"
|
|
echo " your TV's DUID) to install, or - easiest - point a Tizen TV browser / URL Launcher"
|
|
echo " at \`https://<your-instance>/player\` (no signing needed)."
|
|
echo "- \`autorun.zip\` - BrightSign player installer. Drop it on the root of a player's"
|
|
echo " storage (microSD, USB, or internal flash) and power-cycle: it unpacks itself and"
|
|
echo " reboots into the player. Edit \`screentinker.json\` inside the archive first to"
|
|
echo " point it at your own server."
|
|
if [ "${{ steps.ver.outputs.prerelease }}" = "true" ]; then
|
|
echo "- Docker image: \`ghcr.io/screentinker/screentinker:${{ steps.ver.outputs.version }}\` (pre-release - \`:latest\` is NOT moved)."
|
|
else
|
|
echo "- Docker image: \`ghcr.io/screentinker/screentinker:${{ steps.ver.outputs.version }}\` (also \`:latest\`)."
|
|
fi
|
|
echo "- \`ScreenTinker.apk\` - signed Android player (attached during release finalization)."
|
|
echo "- \`screentinker-sbom-${{ steps.ver.outputs.version }}.cdx.json\` - CycloneDX 1.5 software bill of materials for the server's production dependencies, with the licence of every component."
|
|
} > RELEASE_NOTES.md
|
|
cat RELEASE_NOTES.md
|
|
|
|
- name: Create GitHub Release
|
|
env:
|
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
run: |
|
|
# #80: pre-release tags publish as a GitHub *pre-release* (not "Latest"),
|
|
# which also keeps the /releases/latest API pointing at the last stable.
|
|
PRERELEASE_FLAG=""
|
|
[ "${{ steps.ver.outputs.prerelease }}" = "true" ] && PRERELEASE_FLAG="--prerelease"
|
|
gh release create "${{ steps.ver.outputs.tag }}" \
|
|
$PRERELEASE_FLAG \
|
|
--title "ScreenTinker ${{ steps.ver.outputs.tag }}" \
|
|
--notes-file RELEASE_NOTES.md \
|
|
"${TARBALL}" \
|
|
autorun.zip \
|
|
"screentinker-sbom-${{ steps.ver.outputs.version }}.cdx.json" \
|
|
tizen/ScreenTinker.wgt
|
|
|
|
docker:
|
|
name: Docker image (amd64 + arm64) -> ghcr
|
|
needs: test
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v6
|
|
- id: ver
|
|
run: |
|
|
VERSION="$(cat VERSION)"
|
|
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
|
|
# #80: move :latest only for final releases - a pre-release (1.9.0-rc1) must
|
|
# not repoint :latest onto untested code (anyone on :latest pulls it on restart).
|
|
TAGS="ghcr.io/screentinker/screentinker:$VERSION"
|
|
case "$VERSION" in
|
|
*-*) echo "Pre-release $VERSION: :latest will NOT be moved" ;;
|
|
*) TAGS="${TAGS}"$'\n'"ghcr.io/screentinker/screentinker:latest" ;;
|
|
esac
|
|
{ echo "tags<<__EOF__"; printf '%s\n' "$TAGS"; echo "__EOF__"; } >> "$GITHUB_OUTPUT"
|
|
- uses: docker/setup-qemu-action@v3
|
|
- uses: docker/setup-buildx-action@v3
|
|
- uses: docker/login-action@v3
|
|
with:
|
|
registry: ghcr.io
|
|
username: ${{ github.actor }}
|
|
password: ${{ secrets.GITHUB_TOKEN }}
|
|
# ghcr refused this push on the 1.9.29 release with "denied: permission_denied: Error from
|
|
# intermediary with HTTP status code 403", then accepted the identical build on a manual
|
|
# re-run minutes later. Nothing about the token, the permissions or the workflow changed in
|
|
# between — the registry simply said no once.
|
|
#
|
|
# That is worth one retry rather than a failed release, and it is worst exactly here: the
|
|
# GitHub Release job has already published by this point, so a failure leaves a tag that
|
|
# exists with no image behind it. Anyone deploying from ghcr — alpha, and every self-hoster
|
|
# pulling :latest — sees a version that is announced and unpullable, which reads as a broken
|
|
# release rather than a hiccup at a registry.
|
|
- uses: docker/build-push-action@v6
|
|
id: push
|
|
continue-on-error: true
|
|
with:
|
|
context: .
|
|
platforms: linux/amd64,linux/arm64
|
|
push: true
|
|
tags: ${{ steps.ver.outputs.tags }}
|
|
|
|
- name: Pause before retrying the push
|
|
if: steps.push.outcome == 'failure'
|
|
run: sleep 45
|
|
|
|
# No continue-on-error: a second refusal is a real failure and must fail the release.
|
|
- name: Retry the push
|
|
if: steps.push.outcome == 'failure'
|
|
uses: docker/build-push-action@v6
|
|
with:
|
|
context: .
|
|
platforms: linux/amd64,linux/arm64
|
|
push: true
|
|
tags: ${{ steps.ver.outputs.tags }}
|
|
|
|
# TODO (deferred): build + sign the Android APK in CI. Requires the release
|
|
# keystore + passwords as encrypted Actions secrets. For now the maintainer
|
|
# attaches a signed APK out-of-band (and self-hosters mount one at
|
|
# /data/ScreenTinker.apk).
|