Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .github/workflows/pr-title.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
name: PR Title

on:
pull_request:
types: [opened, edited, synchronize, reopened]

permissions:
pull-requests: read

jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: amannn/action-semantic-pull-request@0723387faaf9b38adef4775cd42cfd5155ed6017 # v5.5.3
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
144 changes: 131 additions & 13 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,102 @@ on:
push:
tags: ["v*"]
# Manual trigger: builds the version in src-tauri/tauri.conf.json and creates
# the v<version> tag + draft release (tauri-action expands __VERSION__).
# a draft v<version> release.
workflow_dispatch:
# Called by version.yml on merge to main with a computed version, which is
# written into tauri.conf.json at build time (not committed; tags are the
# source of truth).
workflow_call:
inputs:
version:
description: "Version to release, e.g. 0.2.0 (written into tauri.conf.json)"
required: true
type: string
notes:
description: "Release body / notes (Markdown)"
required: false
type: string
default: ""
publish:
description: "Flip the draft to a published release"
required: false
type: boolean
default: false

permissions: {}

jobs:
# Pre-create the draft release once so the parallel matrix builds upload to it
# by releaseId. Creating-by-tag from multiple matrix jobs races and can produce
# duplicate/partial releases (tauri-action#914), which is unsafe on the
# auto-publish path.
create-release:
runs-on: ubuntu-latest
permissions:
contents: write
outputs:
version: ${{ steps.v.outputs.version }}
release_id: ${{ steps.create.outputs.result }}
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
persist-credentials: false

# Resolve the version for every trigger: workflow_call input, then a pushed
# v* tag, else the committed tauri.conf.json version (manual dispatch).
- id: v
shell: bash
env:
IN_VERSION: ${{ inputs.version }}
GH_REF: ${{ github.ref }}
run: |
set -euo pipefail
if [ -n "$IN_VERSION" ]; then
version="$IN_VERSION"
elif [ "${GH_REF#refs/tags/v}" != "$GH_REF" ]; then
version="${GH_REF#refs/tags/v}"
else
version="$(jq -r '.version' src-tauri/tauri.conf.json)"
fi
echo "version=$version" >> "$GITHUB_OUTPUT"
echo "Resolved release version: $version"

- id: create
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0
env:
VERSION: ${{ steps.v.outputs.version }}
IN_NOTES: ${{ inputs.notes }}
with:
script: |
const version = process.env.VERSION;
const tag = `v${version}`;
const notes = (process.env.IN_NOTES && process.env.IN_NOTES.length)
? process.env.IN_NOTES
: 'See the assets below to download and install.\n\nInstallers are unsigned until code-signing credentials are configured; macOS Gatekeeper and Windows SmartScreen may warn on first launch.';
const { owner, repo } = context.repo;
// Reuse an existing release for this tag so re-runs are idempotent
// (listReleases includes drafts; getReleaseByTag does not).
const releases = await github.paginate(github.rest.repos.listReleases, { owner, repo, per_page: 100 });
const existing = releases.find((r) => r.tag_name === tag);
if (existing) {
core.info(`Reusing existing release ${existing.id} for ${tag}`);
return existing.id;
}
const { data } = await github.rest.repos.createRelease({
owner,
repo,
tag_name: tag,
target_commitish: context.sha,
name: `HyperDeck Adapter ${tag}`,
body: notes,
draft: true,
prerelease: false,
});
core.info(`Created draft release ${data.id} for ${tag}`);
return data.id;

build:
needs: create-release
strategy:
fail-fast: false
matrix:
Expand All @@ -32,10 +121,24 @@ jobs:
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
persist-credentials: false

# Build the resolved version so the bundle/installer version matches the
# release tag on every trigger. Not committed — git tags are the source of
# truth.
- name: Set version
shell: bash
env:
VERSION: ${{ needs.create-release.outputs.version }}
run: |
set -euo pipefail
tmp=$(mktemp)
jq --arg v "$VERSION" '.version = $v' tauri.conf.json > "$tmp"
mv "$tmp" tauri.conf.json
echo "Set tauri.conf.json version to $VERSION"

- uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.platform == 'macos-latest' && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }}
Expand Down Expand Up @@ -109,23 +212,38 @@ jobs:
echo "No Apple signing identity configured: app will be ad-hoc signed."
fi

- name: Build and publish draft release
# Uploads bundles to the pre-created draft release by id (no tag race).
- name: Build and upload bundles
uses: tauri-apps/tauri-action@84b9d35b5fc46c1e45415bdb6144030364f7ebc5 # v0.6.2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
projectPath: src-tauri
tagName: v__VERSION__
releaseName: "HyperDeck Adapter v__VERSION__"
releaseBody: |
See the assets below to download and install.

Installers are unsigned until code-signing credentials are
configured; macOS Gatekeeper and Windows SmartScreen may warn on
first launch.
releaseDraft: true
prerelease: false
releaseId: ${{ needs.create-release.outputs.release_id }}
args: >-
${{ matrix.args }}
${{ env.UPDATER_CONFIG }}
${{ env.WIN_CERT_THUMBPRINT != '' && format('--config {{"bundle":{{"windows":{{"certificateThumbprint":"{0}"}}}}}}', env.WIN_CERT_THUMBPRINT) || '' }}

# Auto-release path only: flip the draft to published once both platform builds
# have uploaded their assets. Manual dispatch / tag-push leave publish unset, so
# those releases stay drafts for review.
publish:
needs: [create-release, build]
if: ${{ inputs.publish }}
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0
env:
RELEASE_ID: ${{ needs.create-release.outputs.release_id }}
with:
script: |
await github.rest.repos.updateRelease({
owner: context.repo.owner,
repo: context.repo.repo,
release_id: Number(process.env.RELEASE_ID),
draft: false,
});
core.info(`Published release ${process.env.RELEASE_ID}`);
70 changes: 70 additions & 0 deletions .github/workflows/version.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
name: Auto Release

on:
push:
branches: [main]

permissions:
contents: read

concurrency:
group: auto-release
cancel-in-progress: false

jobs:
compute:
runs-on: ubuntu-latest
outputs:
new_tag: ${{ steps.out.outputs.new_tag }}
new_version: ${{ steps.out.outputs.new_version }}
changelog: ${{ steps.tag.outputs.changelog }}
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
fetch-depth: 0
persist-credentials: false
- id: tag
uses: mathieudutour/github-tag-action@d28fa2ccfbd16e871a4bdf35e11b3ad1bd56c0c1 # v6.2
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
dry_run: true
# default_bump: false is load-bearing: it makes a merge with no
# feat/fix/breaking commit produce an empty new_tag, which is what
# skips the release. Do not remove it or docs-only merges will release.
default_bump: false
release_branches: main
fetch_all_tags: true
- id: out
shell: bash
env:
PREV_VERSION: ${{ steps.tag.outputs.previous_version }}
NEW_VERSION: ${{ steps.tag.outputs.new_version }}
NEW_TAG: ${{ steps.tag.outputs.new_tag }}
run: |
set -euo pipefail
if [ -z "$NEW_TAG" ]; then
echo "No releasable commits since the last tag; skipping release."
{
echo "new_tag="
echo "new_version="
} >> "$GITHUB_OUTPUT"
exit 0
fi
version="$(bash build/ci/clamp_version.sh "$PREV_VERSION" "$NEW_VERSION")"
{
echo "new_version=$version"
echo "new_tag=v$version"
} >> "$GITHUB_OUTPUT"
echo "Release $version (previous $PREV_VERSION; action proposed $NEW_VERSION)"

release:
needs: compute
if: needs.compute.outputs.new_tag != ''
permissions:
contents: write
uses: ./.github/workflows/release.yml
with:
version: ${{ needs.compute.outputs.new_version }}
notes: ${{ needs.compute.outputs.changelog }}
publish: true
secrets: inherit
26 changes: 26 additions & 0 deletions build/ci/clamp_version.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
#!/usr/bin/env bash
# Clamp a computed semantic version to the 0.x range.
#
# github-tag-action sends 0.x + a breaking change to 1.0.0; we stay in 0.x
# until 1.0.0 is shipped deliberately. When the previous version is 0.x and the
# computed version is not, replace it with a minor bump of the previous version
# (0.<minor>.<patch> -> 0.<minor+1>.0). Otherwise pass the computed version
# through unchanged.
#
# Usage: clamp_version.sh <previous_version> <computed_version>
# Inputs must be bare semver (no leading "v"); the caller strips any prefix.
set -euo pipefail

prev="${1:?previous_version required}"
computed="${2:?computed_version required}"

prev_major="${prev%%.*}"
computed_major="${computed%%.*}"

if [ "$prev_major" = "0" ] && [ "$computed_major" != "0" ]; then
rest="${prev#*.}" # <minor>.<patch>
prev_minor="${rest%%.*}" # <minor>
printf '0.%s.0\n' "$((prev_minor + 1))"
else
printf '%s\n' "$computed"
fi
27 changes: 27 additions & 0 deletions build/ci/clamp_version_test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
#!/usr/bin/env bash
set -euo pipefail
here="$(cd "$(dirname "$0")" && pwd)"
clamp="$here/clamp_version.sh"

fail=0
check() {
local desc="$1" prev="$2" computed="$3" want="$4" got
got="$(bash "$clamp" "$prev" "$computed")"
if [ "$got" = "$want" ]; then
printf 'ok - %s\n' "$desc"
else
printf 'FAIL - %s: clamp(%s, %s) = %s, want %s\n' "$desc" "$prev" "$computed" "$got" "$want"
fail=1
fi
}

check "breaking change stays in 0.x" 0.1.0 1.0.0 0.2.0
check "feat minor within 0.x passes through" 0.1.0 0.2.0 0.2.0
check "fix patch within 0.x passes through" 0.1.0 0.1.1 0.1.1
check "breaking at higher 0.x minor" 0.9.0 1.0.0 0.10.0
check "breaking ignores prev patch digit" 0.9.3 1.0.0 0.10.0
check "minor zero bumps to 0.1.0" 0.0.4 1.0.0 0.1.0
check "already 1.x passes through" 1.2.0 2.0.0 2.0.0
check "1.x minor passes through" 1.2.0 1.3.0 1.3.0

exit "$fail"
Loading
Loading