diff --git a/.github/workflows/mobile.yml b/.github/workflows/mobile.yml index 74672ee0..025699f7 100644 --- a/.github/workflows/mobile.yml +++ b/.github/workflows/mobile.yml @@ -23,6 +23,11 @@ on: - "scripts/check-sdk-versions.sh" - ".github/workflows/mobile.yml" workflow_dispatch: + inputs: + publish_version: + description: "Android package version to publish from main (for release recovery)" + required: false + type: string env: CARGO_TERM_COLOR: always @@ -256,13 +261,20 @@ jobs: name: orch8-mobile.aar path: packages/android/orch8-mobile/build/outputs/aar/orch8-mobile-release.aar - name: Publish Android package - if: startsWith(github.ref, 'refs/tags/v') + if: startsWith(github.ref, 'refs/tags/v') || inputs.publish_version != '' working-directory: packages/android env: GITHUB_ACTOR: ${{ github.actor }} GITHUB_TOKEN: ${{ github.token }} - ORCH8_MOBILE_VERSION: ${{ github.ref_name }} - run: gradle :orch8-mobile:publishReleasePublicationToGitHubPackagesRepository + ORCH8_MOBILE_VERSION: ${{ inputs.publish_version || github.ref_name }} + run: | + requested_version="${ORCH8_MOBILE_VERSION#v}" + checked_in_version="$(sed -n 's/^VERSION_NAME=//p' gradle.properties)" + if [[ "$requested_version" != "$checked_in_version" ]]; then + echo "::error::Requested Android package version $requested_version does not match VERSION_NAME=$checked_in_version." + exit 1 + fi + gradle :orch8-mobile:publishReleasePublicationToGitHubPackagesRepository # GitHub Release creation and native release assets are owned exclusively by # release.yml. Keeping a second publisher here creates a race and can produce diff --git a/packages/android/orch8-mobile/build.gradle.kts b/packages/android/orch8-mobile/build.gradle.kts index 7bcf7452..4345ffb5 100644 --- a/packages/android/orch8-mobile/build.gradle.kts +++ b/packages/android/orch8-mobile/build.gradle.kts @@ -5,8 +5,11 @@ plugins { } group = "io.orch8" -version = providers.gradleProperty("VERSION_NAME") - .orElse(providers.environmentVariable("ORCH8_MOBILE_VERSION")) +// A release tag is authoritative. Keep the checked-in VERSION_NAME as the +// local/CI default, but never let it override the version supplied by the +// tag-publishing workflow. +version = providers.environmentVariable("ORCH8_MOBILE_VERSION") + .orElse(providers.gradleProperty("VERSION_NAME")) .orElse("0.0.0-local") .get() .removePrefix("v") diff --git a/packages/flutter/.github/workflows/publish.yml b/packages/flutter/.github/workflows/publish.yml index bb4be9a3..2f7ab8cb 100644 --- a/packages/flutter/.github/workflows/publish.yml +++ b/packages/flutter/.github/workflows/publish.yml @@ -3,9 +3,50 @@ name: Publish to pub.dev on: push: tags: ["[0-9]+.[0-9]+.[0-9]+"] + workflow_dispatch: jobs: + check: + name: Check registry + runs-on: ubuntu-latest + outputs: + exists: ${{ steps.registry.outputs.exists }} + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - name: Read and validate package version + id: package + shell: bash + run: | + package="$(sed -n 's/^name:[[:space:]]*//p' pubspec.yaml)" + version="$(sed -n 's/^version:[[:space:]]*\([^+[:space:]]*\).*/\1/p' pubspec.yaml)" + test -n "$package" && test -n "$version" + if [[ "$GITHUB_REF_TYPE" == "tag" && "$GITHUB_REF_NAME" != "$version" ]]; then + echo "::error::Tag $GITHUB_REF_NAME does not match pubspec version $version." + exit 1 + fi + echo "package=$package" >> "$GITHUB_OUTPUT" + echo "version=$version" >> "$GITHUB_OUTPUT" + - name: Check whether the version is already published + id: registry + env: + PACKAGE: ${{ steps.package.outputs.package }} + VERSION: ${{ steps.package.outputs.version }} + run: | + if curl -fsS "https://pub.dev/api/packages/${PACKAGE}/versions/${VERSION}" > /dev/null; then + echo "${PACKAGE} ${VERSION} is already on pub.dev; treating this run as verified." + echo "exists=true" >> "$GITHUB_OUTPUT" + else + echo "${PACKAGE} ${VERSION} is not on pub.dev; publication is required." + if [[ "$GITHUB_EVENT_NAME" == "workflow_dispatch" ]]; then + echo "::error::pub.dev only authorizes new publications from version-tag pushes." + exit 1 + fi + echo "exists=false" >> "$GITHUB_OUTPUT" + fi + publish: + needs: check + if: needs.check.outputs.exists != 'true' permissions: id-token: write uses: dart-lang/setup-dart/.github/workflows/publish.yml@v1 diff --git a/packages/react-native/.github/workflows/publish.yml b/packages/react-native/.github/workflows/publish.yml index b3aac45e..97f0d9b2 100644 --- a/packages/react-native/.github/workflows/publish.yml +++ b/packages/react-native/.github/workflows/publish.yml @@ -3,6 +3,7 @@ name: Publish npm package on: push: tags: ["[0-9]+.[0-9]+.[0-9]+"] + workflow_dispatch: permissions: contents: read @@ -21,4 +22,36 @@ jobs: cache: npm - run: npm ci - run: npm run typescript - - run: npm publish --provenance --access public + - name: Read and validate package version + id: package + shell: bash + run: | + package="$(node -p 'require("./package.json").name')" + version="$(node -p 'require("./package.json").version')" + if [[ "$GITHUB_REF_TYPE" == "tag" && "$GITHUB_REF_NAME" != "$version" ]]; then + echo "::error::Tag $GITHUB_REF_NAME does not match package version $version." + exit 1 + fi + echo "package=$package" >> "$GITHUB_OUTPUT" + echo "version=$version" >> "$GITHUB_OUTPUT" + - name: Check whether the version is already published + id: registry + env: + PACKAGE: ${{ steps.package.outputs.package }} + VERSION: ${{ steps.package.outputs.version }} + run: | + if npm view "${PACKAGE}@${VERSION}" version > /dev/null 2>&1; then + echo "${PACKAGE} ${VERSION} is already on npm; treating this run as verified." + echo "exists=true" >> "$GITHUB_OUTPUT" + else + echo "${PACKAGE} ${VERSION} is not on npm; publication is required." + echo "exists=false" >> "$GITHUB_OUTPUT" + fi + - name: Publish npm package + if: steps.registry.outputs.exists != 'true' + run: npm publish --provenance --access public + - name: Verify npm package + env: + PACKAGE: ${{ steps.package.outputs.package }} + VERSION: ${{ steps.package.outputs.version }} + run: test "$(npm view "${PACKAGE}@${VERSION}" version)" = "$VERSION" diff --git a/packages/react-native/package.json b/packages/react-native/package.json index 4ee39dfe..dd7257fe 100644 --- a/packages/react-native/package.json +++ b/packages/react-native/package.json @@ -16,7 +16,10 @@ "typescript": "tsc --noEmit" }, "keywords": ["react-native", "orch8", "workflow", "orchestration"], - "repository": "https://github.com/orch8-io/react-native-orch8", + "repository": { + "type": "git", + "url": "git+https://github.com/orch8-io/react-native-orch8.git" + }, "license": "BUSL-1.1", "publishConfig": { "access": "public", diff --git a/scripts/test-review-fix-guards.sh b/scripts/test-review-fix-guards.sh index 0dd66a17..68bb19d8 100755 --- a/scripts/test-review-fix-guards.sh +++ b/scripts/test-review-fix-guards.sh @@ -161,8 +161,25 @@ assert_contains .github/workflows/release.yml 'bash ../../scripts/embed-aar-lice assert_contains .github/workflows/release.yml 'cp LICENSE bindings/LICENSE' assert_contains .github/workflows/mobile.yml 'cp LICENSE build/Orch8Mobile.xcframework/LICENSE' assert_contains .github/workflows/mobile.yml 'bash ../../scripts/embed-aar-license.sh' +assert_contains .github/workflows/mobile.yml 'publish_version:' +assert_contains .github/workflows/mobile.yml "startsWith(github.ref, 'refs/tags/v') || inputs.publish_version != ''" +assert_contains .github/workflows/mobile.yml 'ORCH8_MOBILE_VERSION: ${{ inputs.publish_version || github.ref_name }}' +assert_contains .github/workflows/mobile.yml 'requested_version="${ORCH8_MOBILE_VERSION#v}"' +assert_contains .github/workflows/mobile.yml 'does not match VERSION_NAME=$checked_in_version' assert_not_contains .github/workflows/mobile.yml 'gh release create' assert_not_contains .github/workflows/mobile.yml 'gh release upload' +assert_contains packages/flutter/.github/workflows/publish.yml 'workflow_dispatch:' +assert_contains packages/flutter/.github/workflows/publish.yml 'https://pub.dev/api/packages/${PACKAGE}/versions/${VERSION}' +assert_contains packages/flutter/.github/workflows/publish.yml "if: needs.check.outputs.exists != 'true'" +assert_contains packages/react-native/.github/workflows/publish.yml 'workflow_dispatch:' +assert_contains packages/react-native/.github/workflows/publish.yml 'npm view "${PACKAGE}@${VERSION}" version' +assert_contains packages/react-native/.github/workflows/publish.yml "if: steps.registry.outputs.exists != 'true'" +assert_contains packages/react-native/package.json '"url": "git+https://github.com/orch8-io/react-native-orch8.git"' +android_version_file="$repo_root/packages/android/orch8-mobile/build.gradle.kts" +release_version_line="$(grep -nF 'providers.environmentVariable("ORCH8_MOBILE_VERSION")' "$android_version_file" | cut -d: -f1)" +fallback_version_line="$(grep -nF 'providers.gradleProperty("VERSION_NAME")' "$android_version_file" | cut -d: -f1)" +[[ -n "$release_version_line" && -n "$fallback_version_line" && "$release_version_line" -lt "$fallback_version_line" ]] \ + || fail "ORCH8_MOBILE_VERSION must take precedence over the checked-in VERSION_NAME" assert_contains packages/android/orch8-mobile/build.gradle.kts 'name.set("Business Source License 1.1")' assert_not_contains packages/android/orch8-mobile/build.gradle.kts 'Apache License 2.0' assert_contains .github/workflows/ci.yml 'Verify Cloud management surface'