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
+555 -36

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
# `go build -tags withsidecar`. Never commit: it's ~130 MB.
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
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:
$(GO) mod download
@@ -29,8 +29,7 @@ sdk-android:
ANDROID_HOME=$(ANDROID_HOME) $(GRADLE) :sdk-android:assembleRelease
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:publish
ANDROID_HOME=$(ANDROID_HOME) $(GRADLE) :sdk-android:publishToMavenLocal
uatu: $(UATU_BIN)
@@ -55,5 +54,18 @@ test-spec-api:
clean:
$(GO) clean
rm -rf bin
rm -rf bin dist pkg/spec-api/dist
$(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
- android
- 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"
)
// Version is stamped at build time via goreleaser ldflags.
// Default "dev" marks untagged local builds.
var Version = "dev"
type testOptions struct {
spec string
bundleID string
@@ -29,6 +33,7 @@ Usage:
Commands:
test Run a spec against an app for a fixed duration.
doctor Check that the host environment is ready to run uatu.
version Print the uatu version.
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)
case "doctor":
return runDoctor(stdout)
case "version", "-v", "--version":
fmt.Fprintln(stdout, Version)
return nil
default:
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) {
err := run([]string{"uatu", "wat"}, io.Discard, io.Discard)
if err == nil || !strings.Contains(err.Error(), "unknown command") {
+5
View File
@@ -3,3 +3,8 @@ org.gradle.parallel=true
org.gradle.caching=true
kotlin.code.style=official
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",
"version": "0.0.1",
"version": "0.0.0-dev",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@uatu/spec",
"version": "0.0.1",
"version": "0.0.0-dev",
"license": "Apache-2.0",
"devDependencies": {
"@types/node": "^22.7.5",
"typescript": "^5.6.3"
"typescript": "^5.9.3"
}
},
"node_modules/@types/node": {
+31 -6
View File
@@ -1,17 +1,42 @@
{
"name": "@uatu/spec",
"version": "0.0.1",
"description": "TypeScript spec API for Uatu (vendored locally; not published to npm in v0.1).",
"private": true,
"version": "0.0.0-dev",
"description": "TypeScript spec API for uatu — a property-based UI fuzzer for mobile apps.",
"type": "module",
"main": "src/index.ts",
"types": "src/index.ts",
"types": "./dist/index.d.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": {
"build": "tsc -p tsconfig.build.json",
"prepublishOnly": "npm run build",
"check": "tsc --noEmit",
"test": "node --test --experimental-strip-types test/*.test.ts"
},
"publishConfig": {
"access": "public"
},
"devDependencies": {
"@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"]
}
+55 -22
View File
@@ -1,9 +1,15 @@
import com.vanniktech.maven.publish.AndroidSingleVariantLibrary
import com.vanniktech.maven.publish.SonatypeHost
plugins {
id("com.android.library") version "8.11.0"
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 {
namespace = "dev.uatu.sdk"
compileSdk = 35
@@ -25,33 +31,60 @@ android {
testOptions {
unitTests.isReturnDefaultValues = true
}
publishing {
singleVariant("release") {
withSourcesJar()
}
}
}
publishing {
publications {
register<MavenPublication>("release") {
groupId = "dev.uatu"
artifactId = "sdk-android"
version = "0.0.1"
afterEvaluate {
from(components["release"])
mavenPublishing {
publishToMavenCentral(SonatypeHost.CENTRAL_PORTAL, automaticRelease = true)
// Sign only when a release-signing key is provided (env or Gradle
// property). Unsigned runs are useful for `publishToMavenLocal` dry-runs;
// CI always has the key set so the actual Central push is always signed.
if (findProperty("signingInMemoryKey") != null) {
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")
}
}
developers {
developer {
id.set("priyanshujain")
name.set("Priyanshu Jain")
url.set("https://github.com/priyanshujain")
}
repositories {
maven {
name = "GitHubPackages"
url = uri("https://maven.pkg.github.com/priyanshujain/uatu")
credentials {
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
targetSdk = 35
versionCode = 1
versionName = "0.0.1"
versionName = findProperty("uatu.version") as String? ?: "0.0.0-dev"
}
compileOptions {
+2
View File
@@ -7,6 +7,8 @@ plugins {
id("com.google.protobuf") version "0.9.5"
}
version = findProperty("uatu.version") as String? ?: "0.0.0-dev"
java {
toolchain {
languageVersion.set(JavaLanguageVersion.of(17))