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
5 changes: 3 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,9 @@ jobs:
run:
working-directory: app
steps:
- uses: actions/checkout@v4
- name: Set temporary keystore path
run: printf 'ANDROID_KEYSTORE_PATH=%s/upload.jks\n' "$RUNNER_TEMP" >> "$GITHUB_ENV"
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
distribution: temurin
Expand Down Expand Up @@ -101,7 +101,8 @@ jobs:
if-no-files-found: error
- name: Remove upload key
if: always()
run: rm -f "$ANDROID_KEYSTORE_PATH"
working-directory: ${{ runner.temp }}
run: rm -f "$RUNNER_TEMP/upload.jks"

build-web:
name: Build Web
Expand Down
2 changes: 1 addition & 1 deletion app/pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name: papyrus
description: "Digital library management application."
publish_to: 'none'
version: 1.0.0+1
version: 0.0.1+1

environment:
sdk: ^3.9.2
Expand Down
4 changes: 2 additions & 2 deletions docs/RELEASING.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ release builds. Normal PR CI continues independently. Manual dispatch on `master
is available for the first build or a retry of an unreleased commit. Tags are
created after builds, not used as a second build trigger. A released tag cannot
be reused for a different commit. Client tags include the build number, e.g.
`v1.0.0+1`. Version changes build Android, web, Linux and Windows as before; the
`v0.0.1+1`. Version changes build Android, web, Linux and Windows as before; the
Android artifact is now a signed AAB, which Google Play turns into device APKs.
The Android artifact is available even if an unrelated desktop job fails.

Expand Down Expand Up @@ -84,7 +84,7 @@ an actionable message; they cannot silently use the debug certificate.
upload key. Keep the same upload key for later releases.
3. Configure the public API, PowerSync, SMTP and Google OAuth settings using the
server deployment runbook. Check the endpoints from outside your local network.
4. Dispatch Release on `master` for the initial `1.0.0+1`, download the
4. Merge the initial `0.0.1+1` version change (or dispatch Release on `master`), download the
`android-release` artifact, and upload `app-release.aab` to internal testing.
The local validation bundle uses a temporary certificate and placeholder
URLs and must **never** be submitted to Play.
Expand Down
9 changes: 7 additions & 2 deletions tools/release_gate.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,11 @@ def parse(text: str, component: str) -> tuple[str, int]:
return version, 0


def decide(current: tuple[str, int], previous: tuple[str, int], component: str) -> bool:
def decide(current: tuple[str, int], previous: tuple[str, int], component: str, *, published: bool = True) -> bool:
if current == previous:
return False
if not published:
return True
Comment on lines +32 to +33

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Limit the no-tag bypass to the bootstrap reset

When the first Android job succeeds but a web/Linux/Windows job fails, the workflow still exposes the signed AAB at .github/workflows/release.yml:97-101, while the release job that creates the tag is skipped because it needs every build job. If that AAB is manually uploaded to Play—as the release documentation permits—subsequent runs still have published=False, so this early return disables both semantic-version and Android build-number monotonicity and can allow reuse of build code 1, which Play rejects. Restrict this exception to the specific initial 1.0.0+1 to 0.0.1+1 reset rather than every tagless history.

Useful? React with 👍 / 👎.

if tuple(map(int, current[0].split("."))) < tuple(map(int, previous[0].split("."))):
raise ValueError("Release version cannot decrease")
if component == "client" and current[1] <= previous[1]:
Expand Down Expand Up @@ -78,7 +80,10 @@ def main() -> None:
if args.manual:
release = True
elif args.base and set(args.base) != {"0"}:
release = decide(current, parse(git("show", f"{args.base}:{path}"), args.component), args.component)
published = bool(git("tag", "--list", "v[0-9]*"))
release = decide(
current, parse(git("show", f"{args.base}:{path}"), args.component), args.component, published=published
)
else:
raise ValueError("A previous commit is required; use a manual build to bootstrap")
version, number = current
Expand Down
20 changes: 20 additions & 0 deletions tools/tests/test_release.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,14 @@ def test_server_dependency_edit_does_not_release(self) -> None:
with self.assertRaises(ValueError):
gate.decide(("0.9.0", 0), first, "server")

def test_initial_version_can_be_reset_before_any_release(self) -> None:
for component, number in (("client", 1), ("server", 0)):
with self.subTest(component=component):
self.assertTrue(gate.decide(("0.0.1", number), ("1.0.0", number), component, published=False))
self.assertFalse(gate.decide(("0.0.1", number), ("0.0.1", number), component, published=False))
with self.assertRaises(ValueError):
gate.decide(("0.0.1", number), ("1.0.0", number), component, published=True)

def test_invalid_manifest_versions_are_rejected(self) -> None:
for version in ("1.0.0", "1.0.0+0", "1.0.0+2100000001", "latest"):
with self.assertRaises(ValueError):
Expand Down Expand Up @@ -114,6 +122,18 @@ def test_compares_committed_values_and_skips_dependency_edits(self) -> None:
self.assertIn("tag=v1.0.0+2", result.stdout)
self.assertIn("release=true", result.stdout)

def test_committed_initial_reset_requires_no_release_tags(self) -> None:
self.manifest.write_text("version: 0.0.1+1\n")
self.commit()
result = self.run_gate("--base", self.base)
self.assertEqual(result.returncode, 0, result.stderr)
self.assertIn("tag=v0.0.1+1", result.stdout)
self.assertIn("release=true", result.stdout)
self.command("tag", "v1.0.0+1", self.base)
result = self.run_gate("--base", self.base)
self.assertNotEqual(result.returncode, 0)
self.assertIn("Release version cannot decrease", result.stderr)

def test_released_tag_cannot_be_reused_for_another_commit(self) -> None:
self.command("tag", "v1.0.0+1")
self.manifest.write_text("version: 1.0.0+1\ndependencies: changed\n")
Expand Down
Loading