diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index aed58e8..d99bf2b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,6 +6,8 @@ on: jobs: build: + permissions: + contents: read runs-on: ubuntu-latest steps: @@ -24,5 +26,5 @@ jobs: - name: Grant execute permission for gradlew run: chmod +x gradlew - - name: Build and Test - run: ./gradlew build + - name: Build, Test, and Verify Release Candidate + run: ./gradlew build verifyReleaseCandidate diff --git a/AGENTS.md b/AGENTS.md index a2c6b41..83336d9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -390,6 +390,17 @@ Verify the published Spring AI adapter API and the starter with one explicitly s ./gradlew verifyPublishedIntegrationConsumer ``` +Run the complete release-candidate gate, including module-local staging and +binary/source/Javadoc/POM/module-metadata artifact checks: + +```bash +./gradlew verifyReleaseCandidate +``` + +Use `-PprojectVersion=0.1.0` for the signed release-candidate path. Non-snapshot +staging requires signing material; the default `0.0.1-SNAPSHOT` path is +secretless. + Run sample app after implementation work: ```bash @@ -436,6 +447,12 @@ Stage and deploy a Central release: ## Update History +### 2026-08-26 + +- Added root publication aggregation and a release-candidate verification gate + that stages every public module and checks the complete artifact/signature + set for the selected version. + ### 2026-08-25 - Connected supported Spring AI 2.0 non-streaming `ChatClient` requests to text-scope adaptation, model/output resolution, conservative preflight, atomic reservation, in-flight tracking, provider usage extraction, and `ReservationAccounting` reconciliation in one around-call lifecycle. diff --git a/build.gradle b/build.gradle index 0595052..2d9ca8c 100644 --- a/build.gradle +++ b/build.gradle @@ -303,6 +303,87 @@ subprojects { } } +def stagingPublicationTasks = publishedModuleDescriptions.keySet().collect { + ":${it}:publishAllPublicationsToStagingRepository" +} + +tasks.register('publishAllPublicationsToStagingRepository') { + group = 'publishing' + description = 'Publishes every public Token Pilot module to its module-local staging repository.' + dependsOn stagingPublicationTasks +} + +tasks.register('verifyReleaseCandidate') { + group = 'verification' + description = 'Runs release checks, stages every public module, and verifies the complete artifact set.' + dependsOn subprojects.collect { "${it.path}:test" } + dependsOn tasks.named('check') + dependsOn tasks.named('publishAllPublicationsToStagingRepository') + + doLast { + def version = project.version.toString() + def requiredArtifacts = [ + 'binary JAR': { String name -> + name.endsWith('.jar') && + !name.endsWith('-sources.jar') && + !name.endsWith('-javadoc.jar') + }, + 'sources JAR': { String name -> name.endsWith('-sources.jar') }, + 'Javadoc JAR': { String name -> name.endsWith('-javadoc.jar') }, + 'POM': { String name -> name.endsWith('.pom') }, + 'Gradle module metadata': { String name -> name.endsWith('.module') } + ] + def requiredSignatures = [ + 'binary JAR signature': { String name -> + name.endsWith('.jar.asc') && + !name.endsWith('-sources.jar.asc') && + !name.endsWith('-javadoc.jar.asc') + }, + 'sources JAR signature': { String name -> name.endsWith('-sources.jar.asc') }, + 'Javadoc JAR signature': { String name -> name.endsWith('-javadoc.jar.asc') }, + 'POM signature': { String name -> name.endsWith('.pom.asc') }, + 'Gradle module metadata signature': { String name -> name.endsWith('.module.asc') } + ] + + publishedModuleDescriptions.keySet().each { moduleName -> + def moduleProject = project(":${moduleName}") + def versionDirectory = moduleProject.file( + "build/staging-deploy/cloud/token-pilot/${moduleName}/${version}" + ) + if (!versionDirectory.isDirectory()) { + throw new GradleException( + "${moduleName} staging directory is missing for version ${version}: ${versionDirectory}" + ) + } + + def artifactNames = versionDirectory.listFiles() + ?.findAll { it.isFile() } + ?.collect { it.name } ?: [] + def missingArtifacts = requiredArtifacts.findAll { label, matcher -> + !artifactNames.any { artifactName -> matcher(artifactName) } + }.keySet() + if (!missingArtifacts.isEmpty()) { + throw new GradleException( + "${moduleName}:${version} is missing published artifacts: ${missingArtifacts}" + ) + } + + if (!version.endsWith('-SNAPSHOT')) { + def missingSignatures = requiredSignatures.findAll { label, matcher -> + !artifactNames.any { artifactName -> matcher(artifactName) } + }.keySet() + if (!missingSignatures.isEmpty()) { + throw new GradleException( + "${moduleName}:${version} is missing release signatures: ${missingSignatures}" + ) + } + } + } + + println "Verified ${publishedModuleDescriptions.size()} Token Pilot release candidates at ${version}." + } +} + def publicationPomTasks = publishedModuleDescriptions.keySet().collect { ":${it}:generatePomFileForMavenJavaPublication" } @@ -346,6 +427,7 @@ tasks.register('verifyPublicationMetadata') { .asFile def pom = new XmlSlurper(false, false).parse(pomFile) + requirePomValue(moduleName, 'version', pom.version.text(), project.version.toString()) requirePomValue(moduleName, 'name', pom.name.text(), moduleName) requirePomValue(moduleName, 'description', pom.description.text(), moduleDescription) requirePomValue(moduleName, 'url', pom.url.text(), 'https://github.com/tokenpliot/tokenpilot')