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
15 changes: 15 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,21 @@ jobs:
- uses: actions/checkout@v4
- run: ./scripts/ci/test_sections.sh check

# A pull request from a release branch (release/X.Y.Z) must be taggable as
# it is: pubspec, CHANGELOG section and SemVer form (#1498).
release-check:
name: Release branch check
if: github.event_name == 'pull_request' && startsWith(github.head_ref, 'release/')
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: check_release.sh for the pubspec version
run: |
VERSION=$(grep '^version:' pubspec.yaml | sed 's/^version:[[:space:]]*//; s/+.*//')
./scripts/release/check_release.sh "$VERSION"

analyze:
name: Code Analysis
timeout-minutes: 20
Expand Down
9 changes: 9 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,15 @@ jobs:
steps:
- uses: actions/checkout@v4

- name: Release checks (scripts/release/check_release.sh)
if: startsWith(github.ref, 'refs/tags/')
run: |
# The same checks as before tagging (#1498); here they only warn, the
# tag is already pushed.
if ! ./scripts/release/check_release.sh "${{ github.ref_name }}" --no-tag-check; then
echo "::warning::scripts/release/check_release.sh found problems with tag ${{ github.ref_name }} (see the log above)."
fi

- name: Warn when git tag and pubspec semver differ
if: startsWith(github.ref, 'refs/tags/')
run: |
Expand Down
28 changes: 22 additions & 6 deletions .github/workflows/version-bump.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,28 @@ jobs:
ref: main
token: ${{ secrets.GITHUB_TOKEN }}

- name: Get current version
# A release branch carrying a beta (`0.5.0-beta.2+7`) keeps its version
# on merge: stripping `-beta.N` and bumping the patch would turn it into
# 0.5.1 (#1498). The bump happens on the next ordinary merge after the
# final version is in pubspec.
- name: Skip pre-release versions
if: steps.check.outputs.merged == 'true'
id: pre
run: |
LINE=$(grep "^version:" pubspec.yaml | sed 's/^version:[[:space:]]*//')
if [[ "${LINE%%+*}" == *-* ]]; then
echo "::notice::pubspec has the pre-release version $LINE; not bumping."
echo "skip=true" >> "$GITHUB_OUTPUT"
else
echo "skip=false" >> "$GITHUB_OUTPUT"
fi

- name: Get current version
if: steps.check.outputs.merged == 'true' && steps.pre.outputs.skip != 'true'
id: current_version
run: |
LINE=$(grep "^version:" pubspec.yaml | sed 's/^version:[[:space:]]*//')
# Drop +build, then any -prerelease (e.g. 0.4.11-b → 0.4.11).
# Drop +build (a -prerelease was skipped above).
CORE=$(echo "$LINE" | sed 's/+.*//; s/-.*//')
if [[ "$LINE" == *'+'* ]]; then
BUILD="${LINE##*+}"
Expand All @@ -50,7 +66,7 @@ jobs:
echo "current_build=$BUILD" >> "$GITHUB_OUTPUT"

- name: Bump patch version
if: steps.check.outputs.merged == 'true'
if: steps.check.outputs.merged == 'true' && steps.pre.outputs.skip != 'true'
id: bump
run: |
CURRENT_VERSION="${{ steps.current_version.outputs.current_version }}"
Expand All @@ -73,15 +89,15 @@ jobs:
echo "new_build=$NEW_BUILD" >> "$GITHUB_OUTPUT"

- name: Update pubspec.yaml
if: steps.check.outputs.merged == 'true'
if: steps.check.outputs.merged == 'true' && steps.pre.outputs.skip != 'true'
run: |
NEW_VERSION="${{ steps.bump.outputs.new_version }}"
NEW_BUILD="${{ steps.bump.outputs.new_build }}"
sed -i "s/^version: .*/version: ${NEW_VERSION}+${NEW_BUILD}/" pubspec.yaml
grep "^version:" pubspec.yaml

- name: Sync AUR PKGBUILD pkgver with pubspec
if: steps.check.outputs.merged == 'true'
if: steps.check.outputs.merged == 'true' && steps.pre.outputs.skip != 'true'
run: |
NEW_VERSION="${{ steps.bump.outputs.new_version }}"
sed -i "s/^pkgver=.*/pkgver=${NEW_VERSION}/" packaging/linux/aur/PKGBUILD
Expand All @@ -90,7 +106,7 @@ jobs:
grep ^pkgver packaging/linux/aur/PKGBUILD

- name: Commit version bump
if: steps.check.outputs.merged == 'true'
if: steps.check.outputs.merged == 'true' && steps.pre.outputs.skip != 'true'
run: |
git config --local user.email "action@github.com"
git config --local user.name "GitHub Action"
Expand Down
79 changes: 79 additions & 0 deletions scripts/release/check_release.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
#!/usr/bin/env bash
# Checks that a release tag can be pushed: the things the Release workflow
# needs, checked before the tag instead of failing in "Publish GitHub Release"
# afterwards (#1498).
#
# scripts/release/check_release.sh 0.5.0-beta.1
# scripts/release/check_release.sh v0.5.0 --no-tag-check # tag already exists (CI)
#
# Checks: the tag is SemVer (X.Y.Z or X.Y.Z-pre); pubspec's version without
# `+build` equals the tag without `v`; a pre-release has a build number in
# pubspec; CHANGELOG.md has `## [<version>]`; the tag is not used yet.
# Then prints what the workflow will do with it. Exits 1 on any problem.
#
# QUERYA_RELEASE_ROOT points at another checkout (tests).
set -euo pipefail

usage() { echo "usage: $0 <tag> [--no-tag-check]" >&2; exit 2; }
[[ $# -ge 1 ]] || usage
TAG="$1"
TAG_CHECK=1
[[ "${2:-}" == "--no-tag-check" ]] && TAG_CHECK=0

SCRIPTS="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
ROOT="${QUERYA_RELEASE_ROOT:-$(cd "$SCRIPTS/.." && pwd)}"
VERSION="${TAG#v}"
problems=0
fail() { echo "✗ $*"; problems=$((problems + 1)); }
ok() { echo "✓ $*"; }

if [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.]+)?$ ]]; then
ok "tag $TAG is a release version"
else
fail "tag $TAG is not X.Y.Z or X.Y.Z-<pre-release> (letters, digits and dots)"
fi
PRERELEASE=0
[[ "$VERSION" == *-* ]] && PRERELEASE=1

LINE="$(grep '^version:' "$ROOT/pubspec.yaml" | sed 's/^version:[[:space:]]*//')"
SEMVER="${LINE%%+*}"
BUILD=""
[[ "$LINE" == *+* ]] && BUILD="${LINE##*+}"
if [[ "$SEMVER" == "$VERSION" ]]; then
ok "pubspec version $LINE matches the tag"
else
fail "pubspec says $LINE; set 'version: $VERSION+<build>' (the artifacts are named after pubspec)"
fi
if (( PRERELEASE )) && ! [[ "$BUILD" =~ ^[0-9]+$ ]]; then
fail "a pre-release needs a build number in pubspec: 'version: $VERSION+<n>'"
fi

if "$SCRIPTS/extract-changelog-section.sh" "$VERSION" "$ROOT/CHANGELOG.md" >/dev/null 2>&1; then
ok "CHANGELOG.md has ## [$VERSION]"
else
fail "CHANGELOG.md has no '## [$VERSION]' section (Publish GitHub Release reads it)"
fi

if (( TAG_CHECK )); then
if git -C "$ROOT" rev-parse -q --verify "refs/tags/$TAG" >/dev/null; then
fail "tag $TAG already exists"
else
ok "tag $TAG is not used yet"
fi
fi

echo
if (( PRERELEASE )); then
echo "The Release workflow will publish a GitHub pre-release (not 'latest'):"
echo " offered in the app only on the Beta / Early Access channel;"
echo " AUR and the APT / DNF repositories are skipped."
else
echo "The Release workflow will publish a stable release and make it 'latest':"
echo " offered on both channels; AUR and APT / DNF are published (when their secrets are set)."
fi

if (( problems )); then
echo
echo "$problems problem(s): fix them before pushing the tag."
exit 1
fi
55 changes: 55 additions & 0 deletions scripts/release/prepare_beta.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
#!/usr/bin/env bash
# Prepares beta N of version X.Y.Z on a release branch (#1498):
# - pubspec: version: X.Y.Z-beta.N+<build + 1>
# - CHANGELOG: a "## [X.Y.Z-beta.N] - <today>" heading under "## [Unreleased]",
# so what was unreleased becomes the beta's notes
# and prints the commands to check, commit and tag. Commits and pushes nothing.
#
# scripts/release/prepare_beta.sh 0.5.0 1
#
# QUERYA_RELEASE_ROOT points at another checkout (tests).
set -euo pipefail

[[ $# -eq 2 ]] || { echo "usage: $0 <X.Y.Z> <N>" >&2; exit 2; }
BASE="$1"
N="$2"
[[ "$BASE" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || { echo "error: '$BASE' is not X.Y.Z" >&2; exit 2; }
[[ "$N" =~ ^[1-9][0-9]*$ ]] || { echo "error: '$N' is not a beta number (1, 2, ...)" >&2; exit 2; }

ROOT="${QUERYA_RELEASE_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}"
VERSION="$BASE-beta.$N"
PUBSPEC="$ROOT/pubspec.yaml"
CHANGELOG="$ROOT/CHANGELOG.md"

LINE="$(grep '^version:' "$PUBSPEC" | sed 's/^version:[[:space:]]*//')"
BUILD=0
[[ "$LINE" == *+* ]] && BUILD="${LINE##*+}"
[[ "$BUILD" =~ ^[0-9]+$ ]] || BUILD=0
NEW="$VERSION+$((BUILD + 1))"

if grep -q "^## \[$VERSION\]" "$CHANGELOG"; then
echo "error: CHANGELOG.md already has ## [$VERSION]" >&2
exit 1
fi
if ! grep -q '^## \[Unreleased\]' "$CHANGELOG"; then
echo "error: CHANGELOG.md has no '## [Unreleased]' heading to put the beta under" >&2
exit 1
fi

sed -i.bak "s/^version: .*/version: $NEW/" "$PUBSPEC" && rm -f "$PUBSPEC.bak"
TODAY="$(date +%Y-%m-%d)"
awk -v heading="## [$VERSION] - $TODAY" '
{ print }
/^## \[Unreleased\]/ && !done { print ""; print heading; done = 1 }
' "$CHANGELOG" > "$CHANGELOG.tmp" && mv "$CHANGELOG.tmp" "$CHANGELOG"

cat <<MSG
pubspec.yaml: version: $NEW
CHANGELOG.md: ## [$VERSION] - $TODAY (the notes under it were [Unreleased])

Next, on the release branch:
scripts/release/check_release.sh $VERSION
git commit -am "release: $VERSION"
git tag -a $VERSION -m "Querya Desktop $VERSION"
git push origin HEAD $VERSION
MSG
119 changes: 119 additions & 0 deletions test/packaging/release_scripts_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import 'dart:io';

import 'package:flutter_test/flutter_test.dart';
import 'package:path/path.dart' as p;

/// scripts/release/check_release.sh and prepare_beta.sh (#1498).
void main() {
// The scripts are bash; the release runs them on Linux runners.
if (Platform.isWindows) return;
late Directory root;

void write(String version, String changelog) {
File(p.join(root.path, 'pubspec.yaml'))
.writeAsStringSync('name: querya_desktop\nversion: $version\n');
File(p.join(root.path, 'CHANGELOG.md')).writeAsStringSync(changelog);
}

ProcessResult run(String script, List<String> args) => Process.runSync(
'bash',
['scripts/release/$script', ...args],
environment: {'QUERYA_RELEASE_ROOT': root.path},
);

const changelog = '# Changelog\n\n## [Unreleased]\n\n### Added\n- Beta thing\n\n'
'## [0.4.22] - 2026-10-01\n\n- Old\n';

setUp(() {
root = Directory.systemTemp.createTempSync('querya_release_');
Process.runSync('git', ['init', '-q', root.path]);
});
tearDown(() => root.deleteSync(recursive: true));

group('check_release.sh', () {
test('a prepared beta passes and is announced as a pre-release', () {
write('0.5.0-beta.1+6', '# Changelog\n\n## [0.5.0-beta.1] - 2026-10-18\n\n- x\n');
final r = run('check_release.sh', ['0.5.0-beta.1']);
expect(r.exitCode, 0, reason: '${r.stdout}');
expect(r.stdout, contains('pre-release'));
expect(r.stdout, contains('AUR and the APT / DNF repositories are skipped'));
});

test('a missing changelog section is named', () {
write('0.5.0-beta.1+6', changelog);
final r = run('check_release.sh', ['0.5.0-beta.1']);
expect(r.exitCode, 1);
expect(r.stdout, contains("no '## [0.5.0-beta.1]' section"));
});

test('a pubspec that does not match the tag fails', () {
write('0.4.22+5', '## [0.5.0]\n');
final r = run('check_release.sh', ['v0.5.0']);
expect(r.exitCode, 1);
expect(r.stdout, contains('pubspec says 0.4.22+5'));
});

test('a beta needs a build number', () {
write('0.5.0-beta.1', '## [0.5.0-beta.1]\n');
final r = run('check_release.sh', ['0.5.0-beta.1']);
expect(r.exitCode, 1);
expect(r.stdout, contains('needs a build number'));
});

test('a tag that is not SemVer fails', () {
write('0.5.0+1', '## [0.5.0]\n');
expect(run('check_release.sh', ['0.5']).exitCode, 1);
expect(run('check_release.sh', ['0.5.0-beta_1']).exitCode, 1);
});

test('an existing tag fails unless --no-tag-check', () {
write('0.5.0+1', '## [0.5.0]\n');
Process.runSync('git', ['-C', root.path, 'add', '.']);
Process.runSync('git', [
'-C', root.path, '-c', 'user.email=t@t', '-c', 'user.name=t',
'commit', '-qm', 'x',
]);
Process.runSync('git', ['-C', root.path, 'tag', '0.5.0']);

final r = run('check_release.sh', ['0.5.0']);
expect(r.exitCode, 1);
expect(r.stdout, contains('already exists'));
expect(run('check_release.sh', ['0.5.0', '--no-tag-check']).exitCode, 0);
});

test('a stable tag is announced as latest', () {
write('0.5.0+9', '## [0.5.0] - 2026-11-01\n');
final r = run('check_release.sh', ['0.5.0']);
expect(r.exitCode, 0, reason: '${r.stdout}');
expect(r.stdout, contains("make it 'latest'"));
});
});

group('prepare_beta.sh', () {
test('sets the version, turns Unreleased into the beta section, then checks out',
() {
write('0.4.22+5', changelog);
final r = run('prepare_beta.sh', ['0.5.0', '1']);
expect(r.exitCode, 0, reason: '${r.stderr}');

expect(File(p.join(root.path, 'pubspec.yaml')).readAsStringSync(),
contains('version: 0.5.0-beta.1+6'));
final notes = File(p.join(root.path, 'CHANGELOG.md')).readAsStringSync();
final unreleased = notes.indexOf('## [Unreleased]');
final beta = notes.indexOf('## [0.5.0-beta.1]');
expect(unreleased, isNonNegative);
expect(beta, greaterThan(unreleased));
expect(notes.indexOf('- Beta thing'), greaterThan(beta));
expect(r.stdout, contains('git tag -a 0.5.0-beta.1'));

expect(run('check_release.sh', ['0.5.0-beta.1']).exitCode, 0);
});

test('refuses a section that already exists and bad arguments', () {
write('0.5.0-beta.1+6', '## [Unreleased]\n\n## [0.5.0-beta.1]\n');
expect(run('prepare_beta.sh', ['0.5.0', '1']).exitCode, 1);
expect(run('prepare_beta.sh', ['0.5', '1']).exitCode, 2);
expect(run('prepare_beta.sh', ['0.5.0', '0']).exitCode, 2);
});
});
}
Loading