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
333 changes: 286 additions & 47 deletions .github/workflows/publish.yml

Large diffs are not rendered by default.

7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,13 @@
- Kotlin and the dependency stack are upgraded for Summon `0.7.0.2`; the CLI and example are now
compiled from real Kotlin source sets and CI rejects expected tasks that report `NO-SOURCE`.

### Fixed

- Maven Central publication now includes the Gradle plugin's real Kotlin sources, validates every
binary component's sources and Javadoc companions before upload, and waits for the Central
deployment to reach `PUBLISHED` before completing the release. Upload IDs are recorded before
polling so interrupted releases resume the exact commit without submitting a duplicate bundle.

### Known limitations

- The Kotlin `2.3.x` wasmWasi artifact is still a Preview1 core module. The WIT contract, guest
Expand Down
1 change: 1 addition & 0 deletions aether-plugin/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ plugins {
java {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
withSourcesJar()
}

tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach {
Expand Down
616 changes: 392 additions & 224 deletions build.gradle.kts

Large diffs are not rendered by default.

13 changes: 12 additions & 1 deletion docs/identity/deployment.md
Original file line number Diff line number Diff line change
Expand Up @@ -340,7 +340,18 @@ intentional property.
## Release verification

Pushes to `main` publish after the automated workflow verification succeeds. `workflow_dispatch`
remains a fallback for rerunning a release from `main`; it is not required for normal publication.
is reserved for a guarded failed-deployment retry or exact-ID resume; it is not required for normal
publication and must not be used to repeat an upload with an unknown outcome.
Before any upload, the workflow publishes into an isolated local Maven repository and verifies that
every non-POM component has its exact primary artifact, sources JAR, and Javadoc JAR. A Central
upload is not considered complete at HTTP acceptance: the workflow polls the deployment until it
reaches `PUBLISHED`, and a validation failure prevents release completion. Exceptional recovery of
an already-tagged but unpublished release requires the exact authenticated `FAILED` deployment ID
and explicit authorization to move the GitHub tag to the corrected commit. Upload and polling are
separate steps: the accepted deployment UUID is recorded immediately and an interrupted deployment
is resumed by that UUID plus the exact commit encoded in its deterministic deployment name. Never
rerun an ambiguous upload. Recovery compares Central's complete component set with the 75-coordinate
release manifest before it can repair a tag or update the GitHub release.
Automated verification includes JVM, wasmJs and wasmWasi guest protocol/crypto tests, the native
OpenSSL host-library tests, PostgreSQL 16 and Firestore conformance/race suites, Summon browser
tests, federation adversarial suites, and the complete example build. Production wasmWasi
Expand Down
82 changes: 77 additions & 5 deletions e2e-tests/tests/config/scaffold.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,11 @@ test('successful main verification publishes automatically and only once per ver

assert.match(workflow, /pull_request:\n\s+branches:\n\s+- main/);
assert.match(workflow, /push:\n\s+branches:\n\s+- main/);
assert.match(workflow, /workflow_dispatch:\n\npermissions:/);
assert.match(workflow, /workflow_dispatch:\n\s+inputs:/);
assert.match(workflow, /retry_failed_deployment_id:/);
assert.match(workflow, /resume_deployment_id:/);
assert.match(workflow, /resume_commit_sha:/);
assert.match(workflow, /repair_release_tag:/);
assert.doesNotMatch(workflow, /hardware_passkey_smoke_|adversarial_review_/);
assert.match(
workflow,
Expand All @@ -89,22 +93,90 @@ test('successful main verification publishes automatically and only once per ver
);
assert.match(
workflow,
/Read release version and publication state[\s\S]*?refs\/tags\/v\$\{VERSION\}[\s\S]*?tag_exists=true[\s\S]*?tag_exists=false/
/Read release version and publication state[\s\S]*?refs\/tags\/v\$\{VERSION\}[\s\S]*?tag_exists=true[\s\S]*?tag_exists=false[\s\S]*?publish_required=true[\s\S]*?publish_required=false/
);
assert.match(workflow, /CHANGELOG_VERSION[\s\S]*?does not match version\.properties/);
assert.match(workflow, /retry_failed_deployment_id and resume_deployment_id are mutually exclusive/);
assert.match(workflow, /Manual publication is recovery-only; provide a failed retry ID or an exact resume ID/);
assert.match(workflow, /resume_commit_sha must be the exact 40-character upload commit/);
assert.match(workflow, /A rerun may duplicate an accepted Central upload/);
assert.match(workflow, /Same-version recovery is restricted to reviewed publication metadata/);
assert.match(workflow, /git diff --name-only "v\$\{VERSION\}"\.\.\.HEAD/);
assert.match(workflow, /git merge-base --is-ancestor "v\$\{VERSION\}" HEAD/);
assert.match(
workflow,
/- name: Publish Aether to Maven Central\n\s+if: steps\.version\.outputs\.tag_exists != 'true'/
/- name: Upload one Aether bundle to Maven Central[\s\S]*?if: steps\.version\.outputs\.upload_required == 'true'/
);
assert.match(
workflow,
/- name: Create and push release tag\n\s+if: steps\.version\.outputs\.tag_exists != 'true'/
/- name: Wait for Maven Central publication[\s\S]*?if: steps\.version\.outputs\.publish_required == 'true'/
);
assert.match(
workflow,
/- name: Create or repair the release tag\n\s+if: steps\.version\.outputs\.publish_required == 'true'/
);
assert.match(workflow, /--force-with-lease="refs\/tags\/v\$\{VERSION\}:\$\{OLD_TAG_OBJECT\}"/);
assert.match(workflow, /- name: Extract latest changelog entry\n\s+id: changelog/);
assert.match(
workflow,
/- name: Create GitHub release\n\s+uses: softprops\/action-gh-release@v2\n\s+if: steps\.changelog\.outputs\.notes != ''/
/- name: Create GitHub release\n\s+uses: softprops\/action-gh-release@v2\n\s+if: steps\.version\.outputs\.publish_required == 'true' && steps\.changelog\.outputs\.notes != ''/
);

const validateIndex = workflow.indexOf('- name: Validate Central recovery request');
const uploadIndex = workflow.indexOf('- name: Upload one Aether bundle to Maven Central');
const waitIndex = workflow.indexOf('- name: Wait for Maven Central publication');
const tagIndex = workflow.indexOf('- name: Create or repair the release tag');
assert.ok(validateIndex < uploadIndex && uploadIndex < waitIndex && waitIndex < tagIndex);
});

test('Maven Central publication requires real sources and waits for PUBLISHED', async () => {
const pluginBuild = await text('../aether-plugin/build.gradle.kts');
const rootBuild = await text('../build.gradle.kts');
const workflow = await text('../.github/workflows/publish.yml');
const signScript = await text('../sign-artifact.sh');

assert.match(pluginBuild, /java\s*\{[\s\S]*?withSourcesJar\(\)/);
assert.match(rootBuild, /verifyCentralPublicationArtifacts by tasks\.registering/);
assert.match(rootBuild, /sources JAR is missing or empty/);
assert.match(rootBuild, /sources JAR contains no Kotlin or Java source/);
assert.match(rootBuild, /val pomOnly = Regex/);
assert.match(rootBuild, /if \(pomOnly\)/);
assert.match(rootBuild, /prepareCentralPortalBundle by tasks\.registering/);
assert.match(rootBuild, /uploadCentralPortalBundle by tasks\.registering/);
assert.match(rootBuild, /waitForCentralPortalPublication by tasks\.registering/);
assert.match(rootBuild, /dependsOn\(verifyCentralPublicationArtifacts, writeExpectedCentralPurls\)/);
assert.match(rootBuild, /publishingType=AUTOMATIC/);
assert.match(rootBuild, /Authorization: \$\{'\$'\}AETHER_CENTRAL_AUTHORIZATION/);
assert.match(rootBuild, /"PUBLISHED" -> \{/);
assert.match(rootBuild, /"FAILED" -> throw GradleException/);
assert.match(rootBuild, /JsonSlurper\(\)\.parseText/);
assert.match(rootBuild, /--connect-timeout 15/);
assert.match(rootBuild, /Central upload outcome is indeterminate/);
assert.match(rootBuild, /numericHttpCode !in setOf\(408, 409, 425, 429\)/);
assert.match(rootBuild, /uploadCentralPortalBundle cannot resume an existing deployment/);
assert.match(rootBuild, /centralExpectedPurls/);
assert.match(rootBuild, /purlList\.size != purls\.size/);
assert.match(rootBuild, /purls != expectedPurls/);
assert.match(signScript, /AETHER_SIGNING_PASSPHRASE/);
assert.match(signScript, /--passphrase-fd 0/);
assert.doesNotMatch(signScript, /PASSPHRASE="\$1"|--passphrase "\$PASSPHRASE"/);
assert.match(workflow, /verifyExpectedSourceTasks verifyCentralPublicationArtifacts check/);
assert.match(workflow, /retry deployment must be FAILED/);
assert.match(workflow, /data\.get\("deploymentName"\) != expected_name/);
assert.match(workflow, /actual_purls != expected_purls/);
assert.match(workflow, /state == "PUBLISHED" or bool\(actual_purls\)/);
assert.match(workflow, /Version \$\{RELEASE_VERSION\} is already public and cannot be replaced/);

const uploadStep = workflow.match(
/- name: Upload one Aether bundle to Maven Central[\s\S]*?(?=\n\s+- name: Wait for Maven Central publication)/
)?.[0] ?? '';
const waitStep = workflow.match(
/- name: Wait for Maven Central publication[\s\S]*?(?=\n\s+- name: Delete publishing credentials)/
)?.[0] ?? '';
assert.doesNotMatch(uploadStep, /MAX_RETRIES|for i in|waitForCentralPortalPublication/);
assert.match(uploadStep, /uploadCentralPortalBundle/);
assert.doesNotMatch(waitStep, /uploadCentralPortalBundle|signingPassword|private-key\.asc/);
assert.match(waitStep, /waitForCentralPortalPublication/);
});

test('the virtual authenticator models a discoverable user-verified CTAP2 passkey', async () => {
Expand Down
12 changes: 6 additions & 6 deletions sign-artifact.sh
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
#!/bin/bash
set -euo pipefail

PASSPHRASE="$1"
KEY_FILE="$2"
OUTPUT_FILE="$3"
INPUT_FILE="$4"
PASSPHRASE="${AETHER_SIGNING_PASSPHRASE:?AETHER_SIGNING_PASSPHRASE is required}"
KEY_FILE="$1"
OUTPUT_FILE="$2"
INPUT_FILE="$3"

GPG_BIN="${GPG_BIN:-$(command -v gpg)}"

Expand Down Expand Up @@ -37,8 +37,8 @@ if [[ -z "$KEY_ID" ]]; then
fi

# Sign the artifact
"$GPG_BIN" --batch --yes --pinentry-mode loopback \
--passphrase "$PASSPHRASE" \
printf '%s\n' "$PASSPHRASE" | "$GPG_BIN" --batch --yes --pinentry-mode loopback \
--passphrase-fd 0 \
--default-key "$KEY_ID" \
--local-user "$KEY_ID" \
--armor --detach-sign \
Expand Down
Loading