Skip to content

Commit a496a2f

Browse files
committed
Publish immutable rules versions to JitPack and verify Maven artifacts
1 parent b2f8998 commit a496a2f

6 files changed

Lines changed: 233 additions & 1 deletion

File tree

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
name: Publish rules to JitPack
2+
on:
3+
push:
4+
branches: [main]
5+
paths:
6+
- rules/manifest.json
7+
- .github/workflows/publish-jitpack.yml
8+
workflow_dispatch:
9+
10+
permissions:
11+
contents: read
12+
13+
concurrency:
14+
group: publish-rules-jitpack
15+
cancel-in-progress: false
16+
17+
jobs:
18+
publish:
19+
if: github.ref == 'refs/heads/main'
20+
runs-on: ubuntu-latest
21+
timeout-minutes: 40
22+
permissions:
23+
contents: write
24+
steps:
25+
- uses: actions/checkout@v7
26+
with:
27+
fetch-depth: 0
28+
- uses: actions/setup-python@v7
29+
with:
30+
python-version: '3.14'
31+
- name: Test release guards and resolve immutable version
32+
id: release
33+
run: |
34+
python3 -B -m unittest discover -s tools -p 'test_*.py'
35+
version=$(python3 tools/release-version.py)
36+
echo "version=$version" >> "$GITHUB_OUTPUT"
37+
if git show-ref --verify --quiet "refs/tags/$version"; then
38+
git checkout --detach "refs/tags/$version"
39+
fi
40+
- uses: actions/setup-java@v6
41+
with:
42+
distribution: zulu
43+
java-version: 17
44+
- uses: android-actions/setup-android@v4
45+
- uses: gradle/actions/setup-gradle@v6
46+
- name: Verify producer baseline and Maven package
47+
env:
48+
VERSION: ${{ steps.release.outputs.version }}
49+
run: |
50+
RULES_ROOT=https://raw.githubusercontent.com/LibChecker/LibChecker-Rules/rules-data
51+
python3 tools/update-rules-bundle.py --check \
52+
--manifest "$RULES_ROOT/releases/$VERSION/manifest.json" --root "$RULES_ROOT"
53+
./gradlew :library:publishToMavenLocal :library:assembleDebugAndroidTest
54+
- name: Create immutable release tag
55+
env:
56+
VERSION: ${{ steps.release.outputs.version }}
57+
run: |
58+
if ! git show-ref --verify --quiet "refs/tags/$VERSION"; then
59+
git config user.name 'github-actions[bot]'
60+
git config user.email '41898282+github-actions[bot]@users.noreply.github.com'
61+
git tag -a "$VERSION" -m "Rules $VERSION"
62+
git push origin "refs/tags/$VERSION"
63+
fi
64+
# GITHUB_TOKEN tag pushes do not start other workflows, so request JitPack here.
65+
- name: Request JitPack build and wait for Maven metadata
66+
env:
67+
VERSION: ${{ steps.release.outputs.version }}
68+
run: |
69+
URL="https://jitpack.io/com/github/LibChecker/LibChecker-Rules-Bundle/$VERSION/LibChecker-Rules-Bundle-$VERSION.pom"
70+
for attempt in {1..30}; do
71+
if curl --fail --location --connect-timeout 15 --max-time 60 "$URL" -o "$RUNNER_TEMP/rules.pom"; then
72+
exit 0
73+
fi
74+
sleep 20
75+
done
76+
echo "::error::JitPack did not publish $VERSION; inspect https://jitpack.io/com/github/LibChecker/LibChecker-Rules-Bundle/$VERSION/build.log"
77+
exit 1
78+
- name: Resolve Maven dependency and verify published AAR
79+
run: ./gradlew -p tools/maven-smoke verifyPublished --refresh-dependencies --no-configuration-cache

‎README.md‎

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,24 @@ keeps the full producer lock at `rules/manifest.json`, outside packaged assets.
220220
It never also stores the ZIP. An existing same-version lock can only be reverified,
221221
not changed; changed data requires a forward version. The scheduled/manual
222222
`.github/workflows/update-rules-bundle.yml` uses the same updater, builds the Bundle
223-
and opens an own-repository PR. It does not publish a release or modify the App.
223+
and opens an own-repository PR. It does not modify the App.
224+
After a rules manifest update reaches `main`, `publish-jitpack.yml` verifies the
225+
immutable producer baseline, builds the Maven package, and creates a numeric tag
226+
matching `dataVersion` (for example, `46`). It then requests the JitPack build and
227+
resolves `com.github.LibChecker:LibChecker-Rules-Bundle:46` through an independent
228+
Gradle consumer, checking that the AAR contains the exact baseline DB and metadata
229+
and the reader class. The tag is the JitPack version; `PROJ_VERSION` is the local
230+
publication version and does not select the JitPack coordinate.
231+
232+
To retry a failed publication, run **Publish rules to JitPack** manually on `main`.
233+
An existing tag is reused without moving it, and a changed manifest under the same
234+
data version is rejected. Library-only releases still need their own version tag;
235+
this workflow automatically releases data-version updates only.
236+
To repeat the Maven acceptance check from the release checkout:
237+
238+
```sh
239+
./gradlew -p tools/maven-smoke verifyPublished --refresh-dependencies --no-configuration-cache
240+
```
241+
224242
New Android icons still use the separate pinned resource sync and
225243
require a resource release; a baseline data update does not create drawables.

‎tools/maven-smoke/build.gradle‎

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import groovy.json.JsonSlurper
2+
import java.security.MessageDigest
3+
import java.util.zip.ZipFile
4+
import java.util.zip.ZipInputStream
5+
6+
def bundleRoot = file('../..')
7+
def manifest = new JsonSlurper().parse(new File(bundleRoot, 'rules/manifest.json'))
8+
def rulesVersion = manifest.dataVersion.toString()
9+
10+
repositories {
11+
maven {
12+
url = uri('https://jitpack.io')
13+
content { includeGroup('com.github.LibChecker') }
14+
}
15+
google()
16+
mavenCentral()
17+
}
18+
19+
configurations {
20+
published
21+
}
22+
23+
dependencies {
24+
published "com.github.LibChecker:LibChecker-Rules-Bundle:${rulesVersion}"
25+
}
26+
27+
tasks.register('verifyPublished') {
28+
doLast {
29+
def artifacts = configurations.published.resolvedConfiguration.resolvedArtifacts
30+
def artifact = artifacts.find {
31+
it.moduleVersion.id.group == 'com.github.LibChecker' &&
32+
it.name == 'LibChecker-Rules-Bundle' && it.moduleVersion.id.version == rulesVersion
33+
}
34+
assert artifact != null && artifact.extension == 'aar': 'Maven did not resolve the release AAR'
35+
new ZipFile(artifact.file).withCloseable { aar ->
36+
assert aar.entries().findAll { it.name.endsWith('/rules.db') }*.name ==
37+
['assets/lcrules/v5/rules.db']: 'Expected exactly one v5 database'
38+
['rules.db', 'metadata.json'].each { name ->
39+
def entry = aar.getEntry("assets/lcrules/v5/${name}")
40+
assert entry != null: "Missing ${name}"
41+
def actual = aar.getInputStream(entry).withCloseable { it.bytes }
42+
def expected = new File(bundleRoot, "library/src/main/assets/lcrules/v5/${name}").bytes
43+
assert Arrays.equals(actual, expected): "Published ${name} differs from the release baseline"
44+
}
45+
def classes = aar.getEntry('classes.jar')
46+
assert classes != null: 'Missing reader classes'
47+
new ZipInputStream(aar.getInputStream(classes)).withCloseable { jar ->
48+
def found = false
49+
for (def entry = jar.nextEntry; entry != null; entry = jar.nextEntry) {
50+
found |= entry.name == 'com/absinthe/rulesbundle/RuleReader.class'
51+
}
52+
assert found: 'Missing RuleReader class'
53+
}
54+
}
55+
def sha256 = MessageDigest.getInstance('SHA-256').digest(artifact.file.bytes).encodeHex()
56+
println "Verified com.github.LibChecker:LibChecker-Rules-Bundle:${rulesVersion}"
57+
println "AAR SHA-256: ${sha256}"
58+
}
59+
}

‎tools/maven-smoke/settings.gradle‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
rootProject.name = 'rules-bundle-maven-smoke'

‎tools/release-version.py‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
#!/usr/bin/env python3
2+
"""Resolve the data-version tag without allowing a published baseline to change."""
3+
import json
4+
from pathlib import Path
5+
import subprocess
6+
7+
8+
def release_version():
9+
manifest = json.loads(Path('rules/manifest.json').read_text())
10+
version = manifest['dataVersion']
11+
if type(version) is not int or version <= 0:
12+
raise ValueError('Release dataVersion must be a positive integer')
13+
tag = f'refs/tags/{version}'
14+
exists = subprocess.run(['git', 'show-ref', '--verify', '--quiet', tag])
15+
if exists.returncode not in (0, 1):
16+
raise RuntimeError('Cannot inspect release tag')
17+
if exists.returncode == 0:
18+
subprocess.run(['git', 'merge-base', '--is-ancestor', tag, 'HEAD'], check=True)
19+
published = subprocess.check_output(['git', 'show', f'{tag}:rules/manifest.json'])
20+
if json.loads(published) != manifest:
21+
raise ValueError(f'Tag {version} already pins different rules; use a new dataVersion')
22+
return str(version)
23+
24+
25+
if __name__ == '__main__':
26+
print(release_version())

‎tools/test_release_version.py‎

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import json
2+
from pathlib import Path
3+
import subprocess
4+
import sys
5+
import tempfile
6+
import unittest
7+
8+
9+
TOOL = Path(__file__).with_name('release-version.py').resolve()
10+
11+
12+
class ReleaseVersionTest(unittest.TestCase):
13+
def test_new_release_retry_and_immutable_tag(self):
14+
with tempfile.TemporaryDirectory() as temporary:
15+
root = Path(temporary)
16+
17+
def git(*args):
18+
return subprocess.check_output(['git', '-C', temporary, *args], stderr=subprocess.PIPE)
19+
20+
def release():
21+
return subprocess.run([sys.executable, str(TOOL)], cwd=root, capture_output=True, text=True)
22+
23+
git('init', '-q')
24+
git('config', 'user.name', 'Release test')
25+
git('config', 'user.email', 'release@example.invalid')
26+
manifest = root / 'rules/manifest.json'
27+
manifest.parent.mkdir()
28+
manifest.write_text(json.dumps({'dataVersion': 46, 'contentSha256': 'first'}))
29+
git('add', '.')
30+
git('commit', '-qm', 'baseline')
31+
self.assertEqual('46\n', release().stdout)
32+
git('tag', '-a', '46', '-m', 'Rules 46')
33+
self.assertEqual(0, release().returncode)
34+
git('commit', '--allow-empty', '-qm', 'unrelated change')
35+
self.assertEqual(0, release().returncode)
36+
manifest.write_text(json.dumps({'dataVersion': 46, 'contentSha256': 'changed'}))
37+
self.assertIn('already pins different rules', release().stderr)
38+
self.assertNotEqual(0, release().returncode)
39+
for invalid in (True, 0, -1, '46', '46; echo invalid'):
40+
manifest.write_text(json.dumps({'dataVersion': invalid}))
41+
self.assertNotEqual(0, release().returncode)
42+
manifest.write_text(json.dumps({'dataVersion': 47}))
43+
result = release()
44+
self.assertEqual(0, result.returncode, result.stderr)
45+
self.assertEqual('47\n', result.stdout)
46+
47+
48+
if __name__ == '__main__':
49+
unittest.main()

0 commit comments

Comments
 (0)