Publisher #112
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Publisher | |
| on: | |
| workflow_dispatch: | |
| inputs: | |
| build_run_id: | |
| description: "Workflow URL or Run ID that produced build artifacts" | |
| required: true | |
| type: string | |
| verify_attestation: | |
| description: "Verify build provenance attestations before publishing" | |
| required: false | |
| type: boolean | |
| default: false | |
| workflow_call: | |
| inputs: | |
| # docs.github.com/en/actions/reference/workflows-and-actions/contexts | |
| run_id: | |
| description: "Workflow run id that produced signed artifacts" | |
| required: true | |
| type: string | |
| vcsver: | |
| description: "Short git version" | |
| required: false | |
| type: string | |
| artifact_subjects: | |
| description: "JSON array of artifact subjects with sha256 digests" | |
| required: false | |
| type: string | |
| sbom_info: | |
| description: "SBOM info JSON blob with subjects and digest" | |
| required: false | |
| type: string | |
| jobs: | |
| publish: | |
| name: 🚚 Publish | |
| runs-on: ubuntu-latest | |
| env: | |
| # docs.github.com/en/actions/reference/workflows-and-actions/contexts#github-context | |
| GROUP_GITHUB: ${{ format('com.github.{0}', github.repository_owner) }} | |
| GROUP_OSSRH: com.celzero | |
| # project artifactId; see: pom.xml | |
| ARTIFACT: firestack | |
| REPO_GITHUB: github | |
| # central.sonatype.org/pages/ossrh-eol | |
| # or "central" | |
| REPO_OSSRH: ossrh | |
| # artefact type | |
| PACK: aar | |
| # final out from make-aar | |
| FOUT: firestack.aar | |
| # artifact bytecode sources | |
| SOURCES: firestack-sources.jar | |
| # POM for Maven Central | |
| POM_OSSRH: ossrhpom.xml | |
| DIST_DIR: dist | |
| RUN_ID: ${{ inputs.run_id || inputs.build_run_id }} | |
| VCSVER_INPUT: ${{ inputs.vcsver }} | |
| # workflow input constants | |
| ARTIFACT_SUBJECTS: ${{ inputs.artifact_subjects }} | |
| SBOM_INFO: ${{ inputs.sbom_info }} | |
| ARTIFACT_PATTERN: "firestack-aar-*" | |
| SBOM_PATTERN: "firestack-sbom-*" | |
| ARTIFACT_PREFIX: "firestack-aar-" | |
| SBOM_PREFIX: "firestack-sbom-" | |
| SBOM_MANIFEST: "manifest.spdx.json" | |
| SBOM_PREDICATE: "https://spdx.dev/Document/v2.2" | |
| # debug symbols zip produced by the debugsymbols Make target; bundled with AAR artifact | |
| FOUTSYM: firestack-debug-symbols.zip | |
| ARCHS: armeabi-v7a arm64-v8a x86 x86_64 | |
| permissions: | |
| contents: read | |
| actions: read | |
| attestations: read | |
| packages: write | |
| steps: | |
| - name: 🥏 Checkout | |
| uses: actions/checkout@v6 | |
| with: | |
| persist-credentials: false | |
| - name: 📀 Metadata | |
| id: runmeta | |
| env: | |
| RUN_ID_OG: ${{ env.RUN_ID }} | |
| RUN_ID: ${{ env.RUN_ID }} | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| set -euo pipefail | |
| # Allow RUN_ID to be passed as a full GitHub Actions run URL. | |
| # Example: https://github.com/celzero/firestack/actions/runs/20923345284 | |
| # Or: https://github.com/celzero/firestack/actions/runs/23057719555/job/66975665220 | |
| RUN_ID="${RUN_ID%%\?*}" # strip query | |
| RUN_ID="${RUN_ID%%\#*}" # strip fragment | |
| RUN_ID="${RUN_ID%/}" # strip trailing slash | |
| case "$RUN_ID" in | |
| *github.com/*/actions/runs/*) | |
| RUN_ID="${RUN_ID#*actions/runs/}" | |
| RUN_ID="${RUN_ID%%/*}" | |
| ;; | |
| esac | |
| echo "::notice::Using Run ID: $RUN_ID (in: $RUN_ID_OG)" | |
| # Export normalized run id for later steps. | |
| printf 'run_id=%s\n' "$RUN_ID" >> "$GITHUB_OUTPUT" | |
| if [ -n "${VCSVER_INPUT:-}" ]; then | |
| printf 'vcsver=%s\n' "${VCSVER_INPUT}" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| info=$(gh run view "$RUN_ID" --json headSha,headBranch,event,displayTitle) | |
| echo "$info" | jq | |
| sha=$(echo "$info" | jq -r '.headSha') | |
| if [ -z "$sha" ] || [ "$sha" = "null" ]; then | |
| echo "::error::unable to resolve head sha for run $RUN_ID" >&2 | |
| exit 11 | |
| fi | |
| # git version (short commit sha) | |
| printf 'sha=%s\n' "$sha" >> "$GITHUB_OUTPUT" | |
| printf 'vcsver=%s\n' "${sha:0:10}" >> "$GITHUB_OUTPUT" | |
| - name: ⬇️ Download artifacts | |
| id: dlaar | |
| uses: actions/download-artifact@v7 | |
| with: | |
| pattern: ${{ env.ARTIFACT_PATTERN }} | |
| run-id: ${{ steps.runmeta.outputs.run_id }} | |
| github-token: ${{ github.token }} | |
| path: ${{ env.DIST_DIR }} | |
| merge-multiple: true | |
| - name: ⬇️ Download SBOM artifact | |
| id: dlsbom | |
| uses: actions/download-artifact@v7 | |
| with: | |
| pattern: ${{ env.SBOM_PATTERN }} | |
| run-id: ${{ steps.runmeta.outputs.run_id }} | |
| github-token: ${{ github.token }} | |
| path: ${{ env.DIST_DIR }} | |
| merge-multiple: true | |
| - name: 🔐 Verify build provenance | |
| if: ${{ inputs.verify_attestation == true || github.event_name == 'workflow_call' }} | |
| env: | |
| REPO: ${{ github.repository }} | |
| ART_DIR: ${{ steps.dlaar.outputs.download-path }} | |
| GH_TOKEN: ${{ github.token }} | |
| SHA: ${{ steps.runmeta.outputs.sha }} | |
| run: | | |
| set -xeuo pipefail | |
| ls -ltr "${ART_DIR}/" | |
| # need to go one dir further for download-artifact v4 not v7 | |
| # ART_DIR="${ART_DIR}/${ARTIFACT_PREFIX}${SHA}" | |
| # ls -ltr "${ART_DIR}/" | |
| file="$ART_DIR/${FOUT}" | |
| if [ ! -f "$file" ]; then | |
| echo "::error::missing artifact $file" >&2 | |
| exit 12 | |
| fi | |
| gh attestation verify "$file" -R "$REPO" | |
| if [ -n "${ARTIFACT_SUBJECTS:-}" ]; then | |
| jq -c '.[]' <<<"${ARTIFACT_SUBJECTS}" | while read -r subject; do | |
| name=$(jq -r '.name' <<<"$subject") | |
| digest=$(jq -r '.digest' <<<"$subject") | |
| file="${ART_DIR}/${name##*/}" | |
| if [ ! -f "$file" ]; then | |
| echo "::error::missing artifact $file for digest check" >&2 | |
| exit 13 | |
| fi | |
| want=${digest#sha256:} | |
| got=$(sha256sum "$file" | awk '{print $1}') | |
| if [ "$got" != "$want" ]; then | |
| echo "::error::digest mismatch for $file (got $got, want $want)" >&2 | |
| exit 14 | |
| fi | |
| done | |
| fi | |
| - name: 🔐 Verify SBOM attestation | |
| if: ${{ inputs.verify_attestation == true || github.event_name == 'workflow_call' }} | |
| env: | |
| REPO: ${{ github.repository }} | |
| ART_DIR: ${{ steps.dlaar.outputs.download-path }} | |
| SBOM_DIR: ${{ steps.dlsbom.outputs.download-path }} | |
| GH_TOKEN: ${{ github.token }} | |
| SHA: ${{ steps.runmeta.outputs.sha }} | |
| run: | | |
| # andrewlock.net/creating-sbom-attestations-in-github-actions/ | |
| set -xeuo pipefail | |
| ls -ltr "${SBOM_DIR}/" | |
| # need to go one dir further for download-artifact v4 not v7 | |
| # SBOM_DIR="${SBOM_DIR}/${SBOM_PREFIX}${SHA}" | |
| # ls -ltr "${SBOM_DIR}/" | |
| if [ -n "${SBOM_INFO:-}" ]; then | |
| name=$(jq -r '.path' <<<"${SBOM_INFO}") | |
| sbom_file="$SBOM_DIR/$(jq -r '.artifactName' <<<"${SBOM_INFO}")/${name}" | |
| digest=$(jq -r '.digest' <<<"${SBOM_INFO}") | |
| # github.com/celzero/firestack/blob/86af89da10abe/.github/workflows/go.yml#L398 | |
| jq -c '.subjects[]' <<<"$SBOM_INFO" | while read -r subject; do | |
| name=$(jq -r '.name' <<<"$subject") | |
| file="${ART_DIR}/${name##*/}" | |
| if [ ! -f "$file" ]; then | |
| echo "::error::missing SBOM subject artifact $file" | |
| exit 14 | |
| fi | |
| gh attestation verify "$file" -R "$REPO" --predicate-type "$predicate" | |
| echo "Verified SBOM subject artifact $file" | |
| done | |
| else | |
| sbom_file=$(find "${SBOM_DIR}" -name "${SBOM_MANIFEST}" -print -quit) | |
| digest=$(cat < $(find "${SBOM_DIR}" -name "${SBOM_MANIFEST}.sha256" -print -quit)) | |
| # verify sbom attestation for the main artifact | |
| file="$ART_DIR/${FOUT}" | |
| if [ ! -f "$file" ]; then | |
| echo "::error::missing artifact $file for SBOM attestation check" >&2 | |
| exit 13 | |
| fi | |
| gh attestation verify "$file" -R "$REPO" --predicate-type "${SBOM_PREDICATE}" | |
| echo "Verified SBOM attestation for artifact $file" | |
| fi | |
| if [ -z "$sbom_file" ]; then | |
| echo "::error::SBOM file not found in ${SBOM_DIR}/" >&2 | |
| exit 15 | |
| fi | |
| if [ -n "$digest" ] && [ "$digest" != "null" ]; then | |
| want=${digest#sha256:} | |
| got=$(sha256sum "$sbom_file" | awk '{print $1}') | |
| if [ "$got" != "$want" ]; then | |
| echo "::error::SBOM digest mismatch (got $got, want $want)" >&2 | |
| exit 16 | |
| fi | |
| else | |
| echo "No SBOM digest; skipping digest verification" >&2 | |
| fi | |
| - name: 🏷️ Setup for GitHub Packages | |
| uses: actions/setup-java@v5 | |
| with: | |
| java-version: '17' | |
| distribution: 'temurin' | |
| # docs.github.com/en/actions/tutorials/build-and-test-code/java-with-maven | |
| # docs.github.com/en/actions/tutorials/publish-packages/publish-java-packages-with-maven#publishing-packages-to-github-packages | |
| - name: 😺 Publish to GitHub Packages | |
| shell: bash | |
| env: | |
| REPOSITORY: ${{ github.repository }} | |
| GITHUB_ACTOR: ${{ github.actor }} | |
| GITHUB_TOKEN: ${{ github.token }} | |
| VCSVER: ${{ steps.runmeta.outputs.vcsver }} | |
| run: | | |
| echo "::notice::Publishing version ${VCSVER} to GitHub Packages" | |
| ls -Rltr "${DIST_DIR}/" | |
| # uploaded at: | |
| # maven.pkg.github.com/celzero/firestack/com/github/celzero/firestack/<commit>/firestack-<commit>.aar | |
| # github.com/deelaa-marketplace/commons-workflow/blob/637dc111/flows/publish-api.yml#L49 | |
| # github.com/markocto/cf-octopub/blob/bba2de2c/github/script/action.yaml#L118 | |
| mvn deploy:deploy-file -X -e \ | |
| -DgroupId="${GROUP_GITHUB}" \ | |
| -DartifactId="${ARTIFACT}" \ | |
| -Dversion="$VCSVER" \ | |
| -Dpackaging="${PACK}" \ | |
| -Dfile="${DIST_DIR}/${FOUT}" \ | |
| -DrepositoryId="${REPO_GITHUB}" \ | |
| -Dsources="${DIST_DIR}/${SOURCES}" \ | |
| -Durl="https://maven.pkg.github.com/${REPOSITORY}" | |
| # central.sonatype.org/publish/publish-portal-api/#authentication-authorization | |
| # github.com/slsa-framework/slsa-github-generator/blob/4876e96b8268/actions/maven/publish/action.yml#L49 | |
| # docs.github.com/en/actions/tutorials/publish-packages/publish-java-packages-with-maven#publishing-packages-to-the-maven-central-repository-and-github-packages | |
| - name: 🏛️ Setup for Maven Central | |
| uses: actions/setup-java@v5 | |
| with: | |
| java-version: '17' | |
| distribution: 'temurin' | |
| server-id: ossrh | |
| server-username: MAVEN_USERNAME | |
| server-password: MAVEN_PASSWORD | |
| gpg-private-key: ${{ secrets.OSSRH_CELZERO_GPG_PRIVATE_KEY }} | |
| gpg-passphrase: ${{ secrets.OSSRH_CELZERO_GPG_PASSPHRASE }} | |
| - name: 📦 Publish to Maven Central | |
| shell: bash | |
| env: | |
| MAVEN_USERNAME: ${{ secrets.OSSRH_USERNAME }} | |
| MAVEN_PASSWORD: ${{ secrets.OSSRH_TOKEN }} | |
| MAVEN_NS: ${{ secrets.OSSRH_CELZERO_NAMESPACE }} | |
| MAVEN_GPG_PASSPHRASE: ${{ secrets.OSSRH_CELZERO_GPG_PASSPHRASE }} | |
| VCSVER: ${{ steps.runmeta.outputs.vcsver }} | |
| run: | | |
| echo "::notice::Publishing version ${VCSVER} to Maven Central" | |
| mvn -f ${POM_OSSRH} versions:set -DnewVersion=${VCSVER} -DgenerateBackupPoms=false | |
| ls -Rltr "${DIST_DIR}/" | |
| # central.sonatype.org/publish/publish-portal-ossrh-staging-api/#getting-started-for-maven-api-like-plugins | |
| # github.com/videolan/vlc-android/blob/c393dd0699/buildsystem/maven/deploy-to-mavencentral.sh#L119 | |
| mvn gpg:sign-and-deploy-file -X -e \ | |
| -DgroupId="${GROUP_OSSRH}" \ | |
| -DartifactId="${ARTIFACT}" \ | |
| -Dversion="$VCSVER" \ | |
| -Dpackaging="${PACK}" \ | |
| -Dfile="${DIST_DIR}/${FOUT}" \ | |
| -DrepositoryId="${REPO_OSSRH}" \ | |
| -DpomFile=${POM_OSSRH} \ | |
| -Dgpg.keyname=C3F3F4A160BB2CFFB5528699F19CE6642C40085C \ | |
| -Dsources="${DIST_DIR}/${SOURCES}" \ | |
| -Durl="https://ossrh-staging-api.central.sonatype.com/service/local/staging/deploy/maven2/" | |
| # central.sonatype.org/publish/publish-portal-api/#authentication-authorization | |
| tok=$(printf "${MAVEN_USERNAME}:${MAVEN_PASSWORD}" | base64) | |
| # central.sonatype.org/publish/publish-portal-ossrh-staging-api/#1-modify-your-ci-script | |
| # central.sonatype.org/publish/publish-portal-ossrh-staging-api/#post-to-manualuploaddefaultrepositorynamespace | |
| # auth required for publishing_type=automatic | |
| curl -D - -X POST -H "Authorization: Bearer ${tok}" \ | |
| "https://ossrh-staging-api.central.sonatype.com/manual/upload/defaultRepository/${GROUP_OSSRH}?publishing_type=automatic" | |
| # github.com/celzero/rethink-app/blob/main/.github/workflows/sym.yml | |
| # firebase.google.com/docs/crashlytics/android/get-started-ndk | |
| - name: 🔥 Upload symbols to Firebase | |
| continue-on-error: true | |
| shell: bash | |
| env: | |
| FIREBASE_TOKEN: ${{ secrets.FIREBASE_TOKEN }} | |
| # comma-separated list of Firebase app IDs (alpha, debug, prod) | |
| FIREBASE_APP_IDS: ${{ secrets.FIREBASE_ALPHA_DEBUG_PROD_APP_ID_CSV }} | |
| VCSVER: ${{ steps.runmeta.outputs.vcsver }} | |
| run: | | |
| set -euo pipefail | |
| echo "::notice::Uploading NDK debug symbols for ${VCSVER} to Firebase Crashlytics" | |
| npm install -g firebase-tools | |
| # eu-unstrip (elfutils) merges a stripped SO with its debug-only companion | |
| # back into a full ELF that Breakpad dump_syms can process. | |
| sudo apt-get install -y --no-install-recommends elfutils | |
| ls -Rltr "${DIST_DIR}/" | |
| # extract debug-only SOs from the symbols zip | |
| unzip -q "${DIST_DIR}/${FOUTSYM}" -d debugsyms | |
| ls -Rltr debugsyms/jni/ | |
| # extract stripped SOs from the distribution AAR (it is a zip) | |
| unzip -q "${DIST_DIR}/${FOUT}" -d stripped_aar | |
| ls -Rltr stripped_aar/jni/ | |
| # merge stripped + debug-only => full unstripped ELF for dump_syms | |
| mkdir -p syms/jni | |
| for arch in $ARCHS; do | |
| stripped="stripped_aar/jni/${arch}/libgojni.so" | |
| debug="debugsyms/jni/${arch}/libgojni.so" | |
| merged="syms/jni/${arch}/libgojni.so" | |
| if [ ! -f "$stripped" ] || [ ! -f "$debug" ]; then | |
| echo "::warning::skipping ${arch}: stripped=$stripped debug=$debug" | |
| continue | |
| fi | |
| mkdir -p "syms/jni/${arch}" | |
| eu-unstrip "$stripped" "$debug" -o "$merged" | |
| echo "merged ${arch}/libgojni.so for Firebase" | |
| done | |
| echo "symbols merged; extracted:" | |
| ls -Rltr syms/jni/ | |
| IFS=',' read -ra APP_IDS <<< "${FIREBASE_APP_IDS}" | |
| for app_id in "${APP_IDS[@]}"; do | |
| app_id="$(echo "$app_id" | tr -d '[:space:]')" | |
| echo "::notice::Uploading symbols for app ${app_id:0:8}...${app_id: -4}" | |
| firebase crashlytics:symbols:upload \ | |
| --app="${app_id}" \ | |
| syms/jni/ | |
| done |