Publish pipeline: goreleaser + Maven Central + npm (#1)

* feat(cli): add Version var and version subcommand

* build(gradle): introduce uatu.version property for lockstep releases

* build(sdk-android): swap GitHub Packages for vanniktech Maven Central plugin

* build(spec-api): make package publish-ready for npm

* ci(release): add goreleaser config for cross-platform uatu CLI builds

* ci: add ci and release GitHub Actions workflows

* ci: restrict ci.yml to PR + workflow_dispatch (no direct push to master)

* docs(release): add local release targets, env example, and install docs

* build(sdk-android): make signAllPublications conditional on signing key

* ci(release): stage sidecar JAR at embed path before go build

* chore(spec-api): regenerate package-lock for updated package.json

* ci: install protoc-gen-go plugins before buf generate

* ci: bump Node to 22 (required for --experimental-strip-types)
This commit is contained in:
pj authored and GitHub committed 2026-04-18 08:50:57 +07:00
1 parent 6e4c832678
commit 0570719e6f
17 files changed
+556 -37

No files matched your search

+29
View File
@@ -0,0 +1,29 @@
# uatu release credentials (local dev only).
#
# Copy to `.env.local` (gitignored) and fill in values ONLY if you need to
# fire a real release from your laptop. Day-to-day work and the `release-cli`
# / `release-android-local` / `release-npm-dry` Make targets don't need any
# of these — they're snapshot/local-only.
#
# In CI, these are provided via GitHub Actions secrets (see .github/workflows/release.yml).
# npm automation token (bypasses 2FA).
# Create at npmjs.com → Access Tokens → Generate New Token → Automation.
NPM_TOKEN=
# Sonatype Central user token (username half).
# Create at central.sonatype.com → Account → Generate User Token.
ORG_GRADLE_PROJECT_mavenCentralUsername=
# Sonatype Central user token (password half).
ORG_GRADLE_PROJECT_mavenCentralPassword=
# ASCII-armored GPG private key for release signing. Include the full
# "-----BEGIN PGP PRIVATE KEY BLOCK-----…-----END PGP PRIVATE KEY BLOCK-----"
# payload, with literal \n newlines escaped inside quotes, e.g.:
# ORG_GRADLE_PROJECT_signingInMemoryKey="-----BEGIN PGP PRIVATE KEY BLOCK-----\nlQVYBG…\n-----END PGP PRIVATE KEY BLOCK-----"
# Generate with: gpg --export-secret-keys --armor <KEYID>
ORG_GRADLE_PROJECT_signingInMemoryKey=
# Passphrase for the GPG key above.
ORG_GRADLE_PROJECT_signingInMemoryKeyPassword=
+72
View File
@@ -0,0 +1,72 @@
name: ci
on:
# Runs on PRs (opened / synchronize / reopened — the defaults) and manual
# dispatch only. We deliberately don't run on direct pushes to master:
# master is PR-merge-only, and PR validation already covers the merge
# commit via the `synchronize` event on the PR branch.
pull_request:
workflow_dispatch:
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version-file: go.mod
cache: true
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: "17"
- name: Set up Android SDK
uses: android-actions/setup-android@v3
- name: Set up Node 22
uses: actions/setup-node@v4
with:
node-version: "22"
cache: npm
cache-dependency-path: pkg/spec-api/package-lock.json
- name: Install buf
uses: bufbuild/buf-setup-action@v1
- name: Install protoc plugins
run: |
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
echo "$(go env GOPATH)/bin" >> "$GITHUB_PATH"
- name: Cache Gradle
uses: actions/cache@v4
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: gradle-${{ runner.os }}-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
restore-keys: |
gradle-${{ runner.os }}-
- name: Bootstrap
run: make bootstrap
- name: Lint proto
run: buf lint
- name: Go vet
run: go vet ./...
- name: Run tests
run: make test
+148
View File
@@ -0,0 +1,148 @@
name: release
on:
push:
tags:
- "v*"
workflow_dispatch:
inputs:
tag:
description: "Tag to release (e.g. v0.0.1-rc1). Must already exist."
required: true
type: string
concurrency:
group: release-${{ github.ref }}
cancel-in-progress: false
jobs:
release-android:
name: Publish sdk-android to Maven Central
runs-on: ubuntu-latest
env:
ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.MAVEN_CENTRAL_USERNAME }}
ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.MAVEN_CENTRAL_PASSWORD }}
ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.SIGNING_IN_MEMORY_KEY }}
ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.SIGNING_IN_MEMORY_KEY_PASSWORD }}
steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.tag || github.ref }}
- name: Resolve version
id: ver
run: |
raw="${{ inputs.tag || github.ref_name }}"
echo "version=${raw#v}" >> "$GITHUB_OUTPUT"
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: "17"
- name: Set up Android SDK
uses: android-actions/setup-android@v3
- name: Cache Gradle
uses: actions/cache@v4
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: gradle-${{ runner.os }}-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
restore-keys: |
gradle-${{ runner.os }}-
- name: Publish to Maven Central
run: ./gradlew :sdk-android:publishAndReleaseToMavenCentral -Puatu.version=${{ steps.ver.outputs.version }}
release-npm:
name: Publish @uatu/spec to npm
runs-on: ubuntu-latest
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.tag || github.ref }}
- name: Resolve version
id: ver
run: |
raw="${{ inputs.tag || github.ref_name }}"
echo "version=${raw#v}" >> "$GITHUB_OUTPUT"
- name: Set up Node 22
uses: actions/setup-node@v4
with:
node-version: "22"
registry-url: "https://registry.npmjs.org"
cache: npm
cache-dependency-path: pkg/spec-api/package-lock.json
- name: Install dependencies
working-directory: pkg/spec-api
run: npm ci
- name: Stamp version
working-directory: pkg/spec-api
run: npm version ${{ steps.ver.outputs.version }} --no-git-tag-version --allow-same-version
- name: Publish
working-directory: pkg/spec-api
# npm tag pre-releases (e.g. 0.1.0-rc1) as "next" so npm install @uatu/spec
# keeps resolving the latest stable.
run: |
if [[ "${{ steps.ver.outputs.version }}" == *-* ]]; then
npm publish --access public --tag next
else
npm publish --access public
fi
release-cli:
name: Publish uatu CLI to GitHub Releases
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.tag || github.ref }}
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version-file: go.mod
cache: true
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: "17"
- name: Set up Android SDK
uses: android-actions/setup-android@v3
- name: Cache Gradle
uses: actions/cache@v4
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: gradle-${{ runner.os }}-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
restore-keys: |
gradle-${{ runner.os }}-
- name: Build sidecar JAR
run: make sidecar
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v6
with:
version: "~> v2"
args: release --clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+6
View File
@@ -30,3 +30,9 @@ node_modules/
# Sidecar fat JAR — build artifact copied in by `make uatu` before # Sidecar fat JAR — build artifact copied in by `make uatu` before
# `go build -tags withsidecar`. Never commit: it's ~130 MB. # `go build -tags withsidecar`. Never commit: it's ~130 MB.
internal/sidecar/assets/sidecar-all.jar internal/sidecar/assets/sidecar-all.jar
# spec-api compiled output
pkg/spec-api/dist/
# goreleaser local output
dist/
+64
View File
@@ -0,0 +1,64 @@
version: 2
project_name: uatu
before:
hooks:
# The Go binary embeds the sidecar fat JAR via //go:embed gated by the
# `withsidecar` build tag. Rebuild the JAR and stage it at the embed
# path so the `go build` below picks up fresh bytes.
- make sidecar
- sh -c 'mkdir -p internal/sidecar/assets && cp sidecar/build/libs/sidecar-all.jar internal/sidecar/assets/sidecar-all.jar'
builds:
- id: uatu
main: ./cmd/uatu
binary: uatu
env:
- CGO_ENABLED=0
flags:
- -tags=withsidecar
ldflags:
- -s -w -X main.Version={{.Version}}
goos:
- darwin
- linux
goarch:
- amd64
- arm64
archives:
- id: uatu
ids:
- uatu
formats: [tar.gz]
name_template: "uatu_{{ .Version }}_{{ .Os }}_{{ .Arch }}"
files:
- README.md
- LICENSE*
checksum:
name_template: checksums.txt
algorithm: sha256
snapshot:
version_template: "{{ incpatch .Version }}-snapshot-{{ .ShortCommit }}"
changelog:
use: github
sort: asc
filters:
exclude:
- "^docs:"
- "^test:"
- "^chore:"
- "^ci:"
- Merge pull request
- Merge branch
release:
github:
owner: priyanshujain
name: uatu
draft: false
prerelease: auto
+16 -4
View File
@@ -11,7 +11,7 @@ SIDECAR_EMBED := internal/sidecar/assets/sidecar-all.jar
SDK_AAR := sdk/android/build/outputs/aar/sdk-android-release.aar SDK_AAR := sdk/android/build/outputs/aar/sdk-android-release.aar
UATU_BIN := bin/uatu UATU_BIN := bin/uatu
.PHONY: bootstrap proto sidecar sdk-android sdk-android-publish uatu test test-go test-kotlin test-spec-api clean .PHONY: bootstrap proto sidecar sdk-android sdk-android-publish uatu test test-go test-kotlin test-spec-api clean release-cli release-android-local release-npm-dry
bootstrap: bootstrap:
$(GO) mod download $(GO) mod download
@@ -29,8 +29,7 @@ sdk-android:
ANDROID_HOME=$(ANDROID_HOME) $(GRADLE) :sdk-android:assembleRelease ANDROID_HOME=$(ANDROID_HOME) $(GRADLE) :sdk-android:assembleRelease
sdk-android-publish: sdk-android-publish:
@if [ -z "$$GH_TOKEN" ]; then echo "GH_TOKEN must be set" >&2; exit 1; fi ANDROID_HOME=$(ANDROID_HOME) $(GRADLE) :sdk-android:publishToMavenLocal
ANDROID_HOME=$(ANDROID_HOME) $(GRADLE) :sdk-android:publish
uatu: $(UATU_BIN) uatu: $(UATU_BIN)
@@ -55,5 +54,18 @@ test-spec-api:
clean: clean:
$(GO) clean $(GO) clean
rm -rf bin rm -rf bin dist pkg/spec-api/dist
$(GRADLE) clean $(GRADLE) clean
# Local release dry-runs. None of these touch remote registries.
release-cli:
$(MAKE) sidecar
goreleaser release --snapshot --clean
release-android-local:
@if [ -f .env.local ]; then set -a; . ./.env.local; set +a; fi; \
ANDROID_HOME=$(ANDROID_HOME) $(GRADLE) :sdk-android:publishToMavenLocal -Puatu.version=0.0.0-local
release-npm-dry:
cd pkg/spec-api && npm ci && npm run build && npm pack --dry-run
+43
View File
@@ -6,3 +6,46 @@ Testing framework and spec in ts/js used for blackbox testing and property based
## Supported Platforms ## Supported Platforms
- android - android
- ios - ios
## Install
### CLI
Download the platform tarball from [GitHub Releases](https://github.com/priyanshujain/uatu/releases/latest):
```sh
# macOS arm64
curl -L https://github.com/priyanshujain/uatu/releases/latest/download/uatu_<version>_darwin_arm64.tar.gz | tar xz
# Linux amd64
curl -L https://github.com/priyanshujain/uatu/releases/latest/download/uatu_<version>_linux_amd64.tar.gz | tar xz
./uatu version
```
Pre-built for `darwin/arm64`, `darwin/amd64`, `linux/amd64`, `linux/arm64`.
### Spec API (npm)
```sh
npm install --save-dev @uatu/spec
```
```ts
import { extract, always, actions } from "@uatu/spec";
```
### Android SDK (Maven Central)
```kotlin
// settings.gradle.kts
dependencyResolutionManagement {
repositories {
mavenCentral()
}
}
// app/build.gradle.kts
dependencies {
implementation("io.github.priyanshujain:sdk-android:<version>")
}
```
+8
View File
@@ -10,6 +10,10 @@ import (
"time" "time"
) )
// Version is stamped at build time via goreleaser ldflags.
// Default "dev" marks untagged local builds.
var Version = "dev"
type testOptions struct { type testOptions struct {
spec string spec string
bundleID string bundleID string
@@ -29,6 +33,7 @@ Usage:
Commands: Commands:
test Run a spec against an app for a fixed duration. test Run a spec against an app for a fixed duration.
doctor Check that the host environment is ready to run uatu. doctor Check that the host environment is ready to run uatu.
version Print the uatu version.
Run "uatu <command> -h" for command-specific flags. Run "uatu <command> -h" for command-specific flags.
` `
@@ -87,6 +92,9 @@ func run(args []string, stdout, stderr io.Writer) error {
return runTest(options, stdout) return runTest(options, stdout)
case "doctor": case "doctor":
return runDoctor(stdout) return runDoctor(stdout)
case "version", "-v", "--version":
fmt.Fprintln(stdout, Version)
return nil
default: default:
return fmt.Errorf("unknown command: %q (try 'uatu help')", args[1]) return fmt.Errorf("unknown command: %q (try 'uatu help')", args[1])
} }
+16
View File
@@ -95,6 +95,22 @@ func TestRun_HelpPrintsUsage(t *testing.T) {
} }
} }
func TestRun_VersionPrintsVersion(t *testing.T) {
prev := Version
Version = "1.2.3-test"
defer func() { Version = prev }()
for _, arg := range []string{"version", "--version", "-v"} {
var stdout bytes.Buffer
if err := run([]string{"uatu", arg}, &stdout, io.Discard); err != nil {
t.Fatalf("%s: %v", arg, err)
}
if strings.TrimSpace(stdout.String()) != "1.2.3-test" {
t.Errorf("%s: got %q, want 1.2.3-test", arg, stdout.String())
}
}
}
func TestRun_UnknownCommand(t *testing.T) { func TestRun_UnknownCommand(t *testing.T) {
err := run([]string{"uatu", "wat"}, io.Discard, io.Discard) err := run([]string{"uatu", "wat"}, io.Discard, io.Discard)
if err == nil || !strings.Contains(err.Error(), "unknown command") { if err == nil || !strings.Contains(err.Error(), "unknown command") {
+5
View File
@@ -3,3 +3,8 @@ org.gradle.parallel=true
org.gradle.caching=true org.gradle.caching=true
kotlin.code.style=official kotlin.code.style=official
android.useAndroidX=true android.useAndroidX=true
# Single source of truth at release time is the git tag (e.g. v0.1.0 → 0.1.0),
# forwarded via `-Puatu.version=...` or `ORG_GRADLE_PROJECT_uatu.version`.
# The default below only applies to untagged local builds.
uatu.version=0.0.0-dev
+39
View File
@@ -0,0 +1,39 @@
# @uatu/spec
TypeScript spec API for [uatu](https://github.com/priyanshujain/uatu) — a property-based UI fuzzer for mobile apps.
Spec authors write specs in TypeScript that describe what an app should *always* do (safety invariants), generate weighted actions to exercise the app, and extract structured state from the accessibility tree. The `uatu` CLI picks up the spec and drives the app under test.
## Install
```sh
npm install --save-dev @uatu/spec
```
## Usage
```ts
import { extract, always, actions, Tap, weighted } from "@uatu/spec";
export const spec = {
extract: extract((tree) => ({
onHomeScreen: tree.some((n) => n.text === "Home"),
})),
always: always(({ state }) => state.onHomeScreen || !state.startedOnHome),
actions: actions(({ tree }) =>
weighted([
[1, Tap(tree.first((n) => n.text === "Checkout"))],
]),
),
};
```
## Version compatibility
`@uatu/spec` is released in lockstep with the uatu CLI. Pin the same major/minor version as your installed `uatu` binary.
## License
Apache-2.0
+4 -3
View File
@@ -1,15 +1,16 @@
{ {
"name": "@uatu/spec", "name": "@uatu/spec",
"version": "0.0.1", "version": "0.0.0-dev",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "@uatu/spec", "name": "@uatu/spec",
"version": "0.0.1", "version": "0.0.0-dev",
"license": "Apache-2.0",
"devDependencies": { "devDependencies": {
"@types/node": "^22.7.5", "@types/node": "^22.7.5",
"typescript": "^5.6.3" "typescript": "^5.9.3"
} }
}, },
"node_modules/@types/node": { "node_modules/@types/node": {
+31 -6
View File
@@ -1,17 +1,42 @@
{ {
"name": "@uatu/spec", "name": "@uatu/spec",
"version": "0.0.1", "version": "0.0.0-dev",
"description": "TypeScript spec API for Uatu (vendored locally; not published to npm in v0.1).", "description": "TypeScript spec API for uatu — a property-based UI fuzzer for mobile apps.",
"private": true,
"type": "module", "type": "module",
"main": "src/index.ts", "types": "./dist/index.d.ts",
"types": "src/index.ts", "main": "./dist/index.js",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
}
},
"files": [
"dist",
"README.md"
],
"repository": {
"type": "git",
"url": "git+https://github.com/priyanshujain/uatu.git",
"directory": "pkg/spec-api"
},
"homepage": "https://github.com/priyanshujain/uatu",
"bugs": {
"url": "https://github.com/priyanshujain/uatu/issues"
},
"license": "Apache-2.0",
"author": "Priyanshu Jain",
"scripts": { "scripts": {
"build": "tsc -p tsconfig.build.json",
"prepublishOnly": "npm run build",
"check": "tsc --noEmit", "check": "tsc --noEmit",
"test": "node --test --experimental-strip-types test/*.test.ts" "test": "node --test --experimental-strip-types test/*.test.ts"
}, },
"publishConfig": {
"access": "public"
},
"devDependencies": { "devDependencies": {
"@types/node": "^22.7.5", "@types/node": "^22.7.5",
"typescript": "^5.6.3" "typescript": "^5.9.3"
} }
} }
+16
View File
@@ -0,0 +1,16 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"noEmit": false,
"emitDeclarationOnly": false,
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"outDir": "./dist",
"rootDir": "./src",
"allowImportingTsExtensions": true,
"rewriteRelativeImportExtensions": true
},
"include": ["src/**/*.ts"],
"exclude": ["test/**/*", "dist/**/*", "node_modules"]
}
+56 -23
View File
@@ -1,9 +1,15 @@
import com.vanniktech.maven.publish.AndroidSingleVariantLibrary
import com.vanniktech.maven.publish.SonatypeHost
plugins { plugins {
id("com.android.library") version "8.11.0" id("com.android.library") version "8.11.0"
kotlin("android") version "2.1.21" kotlin("android") version "2.1.21"
`maven-publish` id("com.vanniktech.maven.publish") version "0.30.0"
} }
version = findProperty("uatu.version") as String? ?: "0.0.0-dev"
group = "io.github.priyanshujain"
android { android {
namespace = "dev.uatu.sdk" namespace = "dev.uatu.sdk"
compileSdk = 35 compileSdk = 35
@@ -25,34 +31,61 @@ android {
testOptions { testOptions {
unitTests.isReturnDefaultValues = true unitTests.isReturnDefaultValues = true
} }
publishing {
singleVariant("release") {
withSourcesJar()
}
}
} }
publishing { mavenPublishing {
publications { publishToMavenCentral(SonatypeHost.CENTRAL_PORTAL, automaticRelease = true)
register<MavenPublication>("release") {
groupId = "dev.uatu" // Sign only when a release-signing key is provided (env or Gradle
artifactId = "sdk-android" // property). Unsigned runs are useful for `publishToMavenLocal` dry-runs;
version = "0.0.1" // CI always has the key set so the actual Central push is always signed.
afterEvaluate { if (findProperty("signingInMemoryKey") != null) {
from(components["release"]) signAllPublications()
}
configure(
AndroidSingleVariantLibrary(
variant = "release",
sourcesJar = true,
publishJavadocJar = true,
),
)
coordinates(
groupId = "io.github.priyanshujain",
artifactId = "sdk-android",
version = version.toString(),
)
pom {
name.set("uatu sdk-android")
description.set(
"Android runtime SDK for uatu — a property-based UI fuzzer for mobile apps. " +
"Exposes a content-provider accessibility bridge consumed by the uatu CLI at test time.",
)
url.set("https://github.com/priyanshujain/uatu")
licenses {
license {
name.set("Apache License, Version 2.0")
url.set("https://www.apache.org/licenses/LICENSE-2.0.txt")
distribution.set("repo")
} }
} }
}
repositories { developers {
maven { developer {
name = "GitHubPackages" id.set("priyanshujain")
url = uri("https://maven.pkg.github.com/priyanshujain/uatu") name.set("Priyanshu Jain")
credentials { url.set("https://github.com/priyanshujain")
username = System.getenv("GH_USERNAME") ?: System.getenv("GITHUB_ACTOR") ?: "priyanshujain"
password = System.getenv("GH_TOKEN") ?: System.getenv("GITHUB_TOKEN") ?: ""
} }
} }
scm {
url.set("https://github.com/priyanshujain/uatu")
connection.set("scm:git:git://github.com/priyanshujain/uatu.git")
developerConnection.set("scm:git:ssh://[email protected]/priyanshujain/uatu.git")
}
} }
} }
+1 -1
View File
@@ -12,7 +12,7 @@ android {
minSdk = 24 minSdk = 24
targetSdk = 35 targetSdk = 35
versionCode = 1 versionCode = 1
versionName = "0.0.1" versionName = findProperty("uatu.version") as String? ?: "0.0.0-dev"
} }
compileOptions { compileOptions {
+2
View File
@@ -7,6 +7,8 @@ plugins {
id("com.google.protobuf") version "0.9.5" id("com.google.protobuf") version "0.9.5"
} }
version = findProperty("uatu.version") as String? ?: "0.0.0-dev"
java { java {
toolchain { toolchain {
languageVersion.set(JavaLanguageVersion.of(17)) languageVersion.set(JavaLanguageVersion.of(17))