Repository navigation
release #6
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: release | |
| # Builds, gates, publishes and install-checks the `encypher` CLI for five | |
| # targets (PRD_CLI_Universal_Install in the private monorepo). The canonical | |
| # copy of every file in this repository is packaging/distribution/ in | |
| # encypherai/encypherai-commercial; packaging/release/public-repo.sh sync puts | |
| # it here, and the provenance job refuses to run when the two differ. | |
| # | |
| # - Dispatch only. `publish: false` is a dry run: every job except publish and | |
| # promote, with the install checks run against the built archives served on | |
| # 127.0.0.1. | |
| # - Trusted code first: .github/scripts/provenance.sh, from this repository, | |
| # refuses a source_ref that is not on the private main before any private | |
| # file executes. | |
| # - The release is published as a pre-release, installed and run by the | |
| # release's own install.sh and install.ps1 on all five platforms, and only | |
| # then marked latest, so an unpinned install never receives an unchecked | |
| # release. | |
| # - This repository is public, and so are its logs and artifacts. Private | |
| # tooling (builds, tests, the artifact gate) runs through | |
| # packaging/release/contained.py, which prints only a status line; a failed | |
| # step's output is uploaded encrypted to the two maintainer certificates in | |
| # .github/, so only a holder of one private half can read it. | |
| on: | |
| workflow_dispatch: | |
| inputs: | |
| source_ref: | |
| description: "Commit of encypherai/encypherai-commercial to release (40 hex characters, on its main)" | |
| required: true | |
| type: string | |
| probe_floor: | |
| description: "ENCYPHER_PROBE_FLOOR: the published probe release head" | |
| required: true | |
| type: string | |
| publish: | |
| description: "Publish the release (false: dry run, nothing is published)" | |
| required: true | |
| default: false | |
| type: boolean | |
| permissions: | |
| contents: read | |
| concurrency: | |
| group: release-${{ github.repository }} | |
| cancel-in-progress: false | |
| env: | |
| RUSTUP_TOOLCHAIN: "1.97.1" | |
| CARGO_TERM_COLOR: never | |
| SOURCE_REPOSITORY: encypherai/encypherai-commercial | |
| jobs: | |
| provenance: | |
| name: Source revision, version and pre-checks | |
| runs-on: ubuntu-24.04 | |
| environment: build | |
| timeout-minutes: 15 | |
| outputs: | |
| revision: ${{ steps.pin.outputs.revision }} | |
| source_date_epoch: ${{ steps.pin.outputs.source_date_epoch }} | |
| version: ${{ steps.pin.outputs.version }} | |
| steps: | |
| - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 | |
| with: | |
| path: public | |
| persist-credentials: false | |
| - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 | |
| with: | |
| repository: ${{ env.SOURCE_REPOSITORY }} | |
| ref: ${{ inputs.source_ref }} | |
| ssh-key: ${{ secrets.ENCYPHER_SOURCE_DEPLOY_KEY }} | |
| path: source | |
| fetch-depth: 0 | |
| filter: tree:0 | |
| sparse-checkout: | | |
| packages/encypher-c2pa | |
| packaging | |
| apps/content-provenance/public/samples | |
| persist-credentials: false | |
| - name: Trusted pre-checks (source on private main, this repository matches it) | |
| env: | |
| SOURCE_REF: ${{ inputs.source_ref }} | |
| SOURCE_DIR: source | |
| PUBLIC_DIR: public | |
| run: bash public/.github/scripts/provenance.sh | |
| - name: Pin the version, revision and SOURCE_DATE_EPOCH | |
| id: pin | |
| working-directory: source | |
| env: | |
| PROBE_FLOOR: ${{ inputs.probe_floor }} | |
| run: | | |
| if ! [[ "${PROBE_FLOOR}" =~ ^[1-9][0-9]*$ ]]; then | |
| echo "probe_floor must be the published probe release head (a number of at least 1), got '${PROBE_FLOOR}'" >&2 | |
| exit 1 | |
| fi | |
| version="$(python3 packaging/release/release.py version)" | |
| if git -C ../public ls-remote --exit-code --tags origin "refs/tags/v${version}" >/dev/null; then | |
| echo "v${version} already exists here; bump encypher-cli, encypher-client and encypher-client-ffi together" >&2 | |
| exit 1 | |
| fi | |
| { | |
| echo "revision=$(git rev-parse HEAD)" | |
| echo "source_date_epoch=$(git log -1 --format=%ct)" | |
| echo "version=${version}" | |
| } >> "$GITHUB_OUTPUT" | |
| - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 | |
| with: | |
| name: smoke-fixture | |
| path: | | |
| source/apps/content-provenance/public/samples/signed-test.jpg | |
| source/packages/encypher-c2pa/fixtures/hash-mode/image_png.png | |
| if-no-files-found: error | |
| retention-days: 7 | |
| windows-lock: | |
| name: Windows unit tests and installer downloads (windows-2022) | |
| needs: provenance | |
| runs-on: windows-2022 | |
| environment: build | |
| timeout-minutes: 45 | |
| defaults: | |
| run: | |
| shell: bash | |
| steps: | |
| - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 | |
| with: | |
| repository: ${{ env.SOURCE_REPOSITORY }} | |
| ref: ${{ needs.provenance.outputs.revision }} | |
| ssh-key: ${{ secrets.ENCYPHER_SOURCE_DEPLOY_KEY }} | |
| path: source | |
| sparse-checkout: | | |
| packages/encypher-c2pa | |
| packaging | |
| persist-credentials: false | |
| - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 | |
| with: | |
| python-version: "3.12" | |
| - name: Install the pinned toolchain | |
| run: rustup toolchain install "${RUSTUP_TOOLCHAIN}" --profile minimal | |
| # The output lock (LockFileEx, POSIX-semantics delete of a held lock | |
| # file, delete-pending contention), the no-replace publish (MoveFileExW) | |
| # and the results-file identity are Windows-only code paths. | |
| - name: Client core and CLI unit tests | |
| working-directory: source/packages/encypher-c2pa | |
| env: | |
| ENCYPHER_PROBE_FLOOR: ${{ inputs.probe_floor }} | |
| run: | | |
| contained="python ../../packaging/release/contained.py --show ^test.result:" | |
| ${contained} --label "encypher-client unit tests" -- cargo test --locked -p encypher-client --lib | |
| ${contained} --label "encypher-cli unit tests" -- cargo test --locked -p encypher-cli --lib | |
| # install.ps1 follows redirects itself and checks every hop before | |
| # requesting it; this runs that downloader under both shells against | |
| # local servers, with a throwaway certificate trusted on this runner only. | |
| # It also checks that install-check.ps1 reads the printed PATH lines | |
| # whole, which Windows PowerShell 5.1's formatting once split. | |
| - name: install.ps1 downloader and PATH lines under Windows PowerShell 5.1 and PowerShell 7 | |
| working-directory: source/packaging/release | |
| env: | |
| ENCYPHER_TEST_TRUST_MACHINE_ROOT: "1" | |
| run: python -m unittest -v test_install.SaveEncypherUrl test_install.InstallPs1PathLine | |
| # The plain logs never leave the runner; these are encrypted to the | |
| # maintainer certificate in this repository. | |
| - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 | |
| if: failure() | |
| with: | |
| name: withheld-logs-windows-lock | |
| path: ${{ runner.temp }}/contained/*.log.cms | |
| if-no-files-found: ignore | |
| retention-days: 7 | |
| build: | |
| name: Build ${{ matrix.target }} | |
| needs: provenance | |
| runs-on: ${{ matrix.os }} | |
| environment: build | |
| timeout-minutes: 90 | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - target: aarch64-apple-darwin | |
| os: macos-15 | |
| - target: x86_64-apple-darwin | |
| os: macos-15-intel | |
| # glibc 2.35 runners: release.py refuses a binary that needs more than GLIBC_2.34. | |
| - target: x86_64-unknown-linux-gnu | |
| os: ubuntu-22.04 | |
| - target: aarch64-unknown-linux-gnu | |
| os: ubuntu-22.04-arm | |
| - target: x86_64-pc-windows-msvc | |
| os: windows-2022 | |
| defaults: | |
| run: | |
| shell: bash | |
| env: | |
| TARGET: ${{ matrix.target }} | |
| ENCYPHER_PROBE_FLOOR: ${{ inputs.probe_floor }} | |
| SOURCE_DATE_EPOCH: ${{ needs.provenance.outputs.source_date_epoch }} | |
| REVISION: ${{ needs.provenance.outputs.revision }} | |
| VERSION: ${{ needs.provenance.outputs.version }} | |
| steps: | |
| - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 | |
| with: | |
| repository: ${{ env.SOURCE_REPOSITORY }} | |
| ref: ${{ needs.provenance.outputs.revision }} | |
| ssh-key: ${{ secrets.ENCYPHER_SOURCE_DEPLOY_KEY }} | |
| path: source | |
| sparse-checkout: | | |
| packages/encypher-c2pa | |
| packaging | |
| persist-credentials: false | |
| - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 | |
| with: | |
| python-version: "3.12" | |
| - name: Install the pinned toolchain and llvm-tools | |
| run: rustup toolchain install "${RUSTUP_TOOLCHAIN}" --profile minimal --target "${TARGET}" --component llvm-tools | |
| - name: Refuse a test-hooks feature graph | |
| working-directory: source/packages/encypher-c2pa | |
| run: | | |
| if ! graph="$(cargo tree --locked -p encypher-cli -p encypher-client-ffi -e features,normal --target "${TARGET}" 2>/dev/null)"; then | |
| echo "cargo tree failed; rerun it at the release's source revision" >&2 | |
| exit 1 | |
| fi | |
| if grep -q 'test-hooks' <<< "${graph}"; then | |
| echo "a shipped build must never enable test-hooks" >&2 | |
| exit 1 | |
| fi | |
| # One remap convention for every pipeline (scripts/remap_prefixes.py) | |
| # keeps the runner's checkout, cargo, rustup and home paths out of the | |
| # binaries; the artifact gate refuses any other absolute source path. | |
| - name: Build the CLI and the C library (dist profile) | |
| working-directory: source/packages/encypher-c2pa | |
| run: | | |
| RUSTFLAGS="$(python scripts/remap_prefixes.py)" | |
| export RUSTFLAGS | |
| contained="python ../../packaging/release/contained.py" | |
| ${contained} --label "dist build: encypher" -- cargo build --locked -p encypher-cli --profile dist --target "${TARGET}" | |
| ${contained} --label "dist build: encypher-client-ffi" -- cargo build --locked -p encypher-client-ffi --profile dist --target "${TARGET}" | |
| # Apple Silicon runs only signed code; the ad hoc signature is the whole | |
| # macOS signing story (no Developer ID, no notarization). | |
| - name: Ad hoc signature (macOS) | |
| if: runner.os == 'macOS' | |
| working-directory: source/packages/encypher-c2pa/target/${{ matrix.target }}/dist | |
| run: codesign --force --sign - encypher libencypher_client.dylib | |
| - name: Artifact gate on the final binaries | |
| working-directory: source/packages/encypher-c2pa | |
| run: | | |
| dir="target/${TARGET}/dist" | |
| case "${TARGET}" in | |
| *windows*) files=("${dir}/encypher.exe" "${dir}/encypher_client.dll") ;; | |
| *apple*) files=("${dir}/encypher" "${dir}/libencypher_client.dylib") ;; | |
| *) files=("${dir}/encypher" "${dir}/libencypher_client.so") ;; | |
| esac | |
| python ../../packaging/release/contained.py --label "artifact gate: binaries" -- python scripts/check-client-artifact.py "${files[@]}" | |
| - name: Smoke the binary | |
| working-directory: source/packages/encypher-c2pa | |
| env: | |
| ENCYPHER_API_KEY: ${{ secrets.ENCYPHER_API_KEY }} | |
| run: | | |
| cli="target/${TARGET}/dist/encypher" | |
| [[ "${TARGET}" == *windows* ]] && cli="${cli}.exe" | |
| "${cli}" --version | |
| "${cli}" inspect fixtures/hash-mode/image_jpeg.jpg --json | |
| if [[ -n "${ENCYPHER_API_KEY}" ]]; then | |
| # The envelopes name documents and service URLs; this log is public. | |
| "${cli}" sign fixtures/hash-mode/image_png.png -o "${RUNNER_TEMP}/smoke-signed.png" \ | |
| --digital-source-type http://cv.iptc.org/newscodes/digitalsourcetype/digitalCapture \ | |
| --on-existing-provenance chain --json >/dev/null | |
| "${cli}" verify "${RUNNER_TEMP}/smoke-signed.png" --json >/dev/null | |
| echo "smoke: signed and verified a PNG" | |
| else | |
| echo "ENCYPHER_API_KEY is not configured: live sign skipped" | |
| fi | |
| - name: Archive and inventory | |
| run: | | |
| python source/packaging/release/release.py package --target "${TARGET}" --version "${VERSION}" \ | |
| --build-dir "source/packages/encypher-c2pa/target/${TARGET}/dist" --revision "${REVISION}" \ | |
| --source-date-epoch "${SOURCE_DATE_EPOCH}" --probe-floor "${ENCYPHER_PROBE_FLOOR}" --out dist | |
| - name: Artifact gate on the archive | |
| run: | | |
| python source/packaging/release/contained.py --label "artifact gate: archive" -- \ | |
| python source/packages/encypher-c2pa/scripts/check-client-artifact.py dist/*.tar.gz | |
| - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 | |
| with: | |
| name: dist-${{ matrix.target }} | |
| path: dist | |
| if-no-files-found: error | |
| retention-days: 7 | |
| - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 | |
| if: failure() | |
| with: | |
| name: withheld-logs-${{ matrix.target }} | |
| path: ${{ runner.temp }}/contained/*.log.cms | |
| if-no-files-found: ignore | |
| retention-days: 7 | |
| merge: | |
| name: SHA256SUMS, inventory and installers | |
| needs: [provenance, windows-lock, build] | |
| runs-on: ubuntu-24.04 | |
| environment: build | |
| timeout-minutes: 20 | |
| steps: | |
| - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 | |
| with: | |
| repository: ${{ env.SOURCE_REPOSITORY }} | |
| ref: ${{ needs.provenance.outputs.revision }} | |
| ssh-key: ${{ secrets.ENCYPHER_SOURCE_DEPLOY_KEY }} | |
| path: source | |
| sparse-checkout: | | |
| packages/encypher-c2pa | |
| packaging | |
| persist-credentials: false | |
| - name: Install the pinned toolchain and llvm-tools | |
| run: rustup toolchain install "${RUSTUP_TOOLCHAIN}" --profile minimal --component llvm-tools | |
| - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 | |
| with: | |
| pattern: dist-* | |
| path: dist | |
| merge-multiple: true | |
| - name: SHA256SUMS and inventory.json (one revision, one toolchain) | |
| run: python3 source/packaging/release/release.py merge --dist dist | |
| - name: Artifact gate on every archive | |
| run: | | |
| python3 source/packaging/release/contained.py --label "artifact gate: all archives" -- \ | |
| python3 source/packages/encypher-c2pa/scripts/check-client-artifact.py dist/*.tar.gz | |
| - name: Attach the installers | |
| run: cp source/packaging/install/install.sh source/packaging/install/install.ps1 dist/ | |
| - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 | |
| with: | |
| name: dist | |
| path: | | |
| dist/*.tar.gz | |
| dist/SHA256SUMS | |
| dist/inventory.json | |
| dist/install.sh | |
| dist/install.ps1 | |
| if-no-files-found: error | |
| retention-days: 7 | |
| - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 | |
| if: failure() | |
| with: | |
| name: withheld-logs-merge | |
| path: ${{ runner.temp }}/contained/*.log.cms | |
| if-no-files-found: ignore | |
| retention-days: 7 | |
| publish: | |
| name: Publish v${{ needs.provenance.outputs.version }} as a pre-release | |
| if: inputs.publish | |
| needs: [provenance, merge] | |
| runs-on: ubuntu-24.04 | |
| environment: publish | |
| timeout-minutes: 15 | |
| permissions: | |
| contents: write | |
| steps: | |
| - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 | |
| with: | |
| name: dist | |
| path: dist | |
| # Every asset is attached to a draft first, then the draft is published: | |
| # with immutable releases on, the files and tag are locked from that | |
| # moment, so nothing can be added or swapped afterwards. | |
| - name: Draft, upload, publish as a pre-release | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| VERSION: ${{ needs.provenance.outputs.version }} | |
| REVISION: ${{ needs.provenance.outputs.revision }} | |
| PROBE_FLOOR: ${{ inputs.probe_floor }} | |
| run: | | |
| toolchain="$(python3 -c 'import json; print(json.load(open("dist/inventory.json"))["toolchain"])')" | |
| notes="encypher ${VERSION}. Source revision ${REVISION}, ${toolchain}, probe floor ${PROBE_FLOOR}. Install: see the README." | |
| gh release create "v${VERSION}" --repo "${GITHUB_REPOSITORY}" --target "${GITHUB_SHA}" --draft --prerelease \ | |
| --title "encypher ${VERSION}" --notes "${notes}" \ | |
| dist/*.tar.gz dist/SHA256SUMS dist/inventory.json dist/install.sh dist/install.ps1 | |
| gh release edit "v${VERSION}" --repo "${GITHUB_REPOSITORY}" --draft=false | |
| # Every leg signs and verifies with the one smoke key, so the legs share | |
| # one organisation's limits (64 outstanding signing plans, a per-caller | |
| # verify concurrency cap). They run one at a time: legs at once got 429s. | |
| install-check: | |
| name: Install on ${{ matrix.name }} | |
| needs: [provenance, merge, publish] | |
| if: always() && needs.merge.result == 'success' && (needs.publish.result == 'success' || (needs.publish.result == 'skipped' && !inputs.publish)) | |
| runs-on: ${{ matrix.os }} | |
| environment: build | |
| timeout-minutes: 30 | |
| strategy: | |
| fail-fast: false | |
| max-parallel: 1 | |
| matrix: | |
| include: | |
| - name: Linux x86_64 and rockylinux:9 | |
| os: ubuntu-22.04 | |
| unpinned: true | |
| - name: Linux arm64 | |
| os: ubuntu-22.04-arm | |
| unpinned: false | |
| - name: macOS Apple Silicon | |
| os: macos-15 | |
| unpinned: true | |
| - name: macOS Intel | |
| os: macos-15-intel | |
| unpinned: false | |
| - name: Windows x64 | |
| os: windows-2022 | |
| unpinned: true | |
| env: | |
| VERSION: ${{ needs.provenance.outputs.version }} | |
| PUBLISHED: ${{ inputs.publish }} | |
| ENCYPHER_API_KEY: ${{ secrets.ENCYPHER_API_KEY }} | |
| steps: | |
| - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 | |
| with: | |
| path: public | |
| persist-credentials: false | |
| - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 | |
| with: | |
| python-version: "3.12" | |
| - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 | |
| with: | |
| name: smoke-fixture | |
| path: fixture-download | |
| - name: Collect the fixtures | |
| shell: bash | |
| run: | | |
| mkdir -p fixture | |
| find fixture-download -type f \( -name signed-test.jpg -o -name image_png.png \) -exec cp {} fixture/ \; | |
| echo "FIXTURE=${GITHUB_WORKSPACE}/fixture" >> "$GITHUB_ENV" | |
| - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 | |
| if: ${{ !inputs.publish }} | |
| with: | |
| name: dist | |
| path: dist | |
| - name: Download base (published release) | |
| if: inputs.publish | |
| shell: bash | |
| run: echo "BASE=https://github.com/${GITHUB_REPOSITORY}/releases" >> "$GITHUB_ENV" | |
| - name: Download base (dry run, loopback mirror of the built release) | |
| if: ${{ !inputs.publish }} | |
| shell: bash | |
| run: | | |
| mirror="${RUNNER_TEMP}/mirror" | |
| mkdir -p "${mirror}/releases/download/v${VERSION}" "${mirror}/releases/latest/download" | |
| cp dist/* "${mirror}/releases/download/v${VERSION}/" | |
| # GitHub serves the latest release's assets here too, and that is | |
| # where an unpinned install reads both the script and SHA256SUMS. | |
| cp dist/* "${mirror}/releases/latest/download/" | |
| { | |
| echo "MIRROR=${mirror}" | |
| echo "BASE=http://127.0.0.1:8765/releases" | |
| echo "ENCYPHER_DOWNLOAD_URL=http://127.0.0.1:8765/releases" | |
| } >> "$GITHUB_ENV" | |
| - name: Start the loopback mirror (Linux, macOS) | |
| if: ${{ !inputs.publish && runner.os != 'Windows' }} | |
| shell: bash | |
| run: | | |
| nohup python -m http.server 8765 --bind 127.0.0.1 --directory "${MIRROR}" >/dev/null 2>&1 & | |
| for _ in $(seq 1 30); do | |
| curl -fsS -o /dev/null "${BASE}/download/v${VERSION}/SHA256SUMS" && exit 0 | |
| sleep 1 | |
| done | |
| exit 1 | |
| - name: Start the loopback mirror (Windows) | |
| if: ${{ !inputs.publish && runner.os == 'Windows' }} | |
| shell: pwsh | |
| run: | | |
| Start-Process python -ArgumentList '-m', 'http.server', '8765', '--bind', '127.0.0.1', '--directory', $env:MIRROR -WindowStyle Hidden | |
| foreach ($i in 1..30) { | |
| try { Invoke-WebRequest -UseBasicParsing "$env:BASE/download/v$env:VERSION/SHA256SUMS" | Out-Null; exit 0 } catch { Start-Sleep 1 } | |
| } | |
| exit 1 | |
| # One leg per operating system takes the README's unpinned command, which | |
| # only a dry run can exercise: until promote, `latest` still points at the | |
| # previous release. | |
| - name: install.sh as a user runs it | |
| if: runner.os != 'Windows' | |
| shell: bash | |
| env: | |
| ENCYPHER_CHECK_UNPINNED: ${{ !inputs.publish && matrix.unpinned && '1' || '' }} | |
| run: bash public/.github/scripts/install-check.sh | |
| - name: install.sh on glibc 2.34 (rockylinux:9) | |
| if: matrix.os == 'ubuntu-22.04' | |
| shell: bash | |
| run: | | |
| docker run --rm --network host -e ENCYPHER_VERSION="${VERSION}" -e ENCYPHER_DOWNLOAD_URL -e BASE \ | |
| -v "${FIXTURE}:/fixture:ro" rockylinux/rockylinux:9 \ | |
| sh -c 'curl -fsSL "${BASE}/download/v${ENCYPHER_VERSION}/install.sh" | sh && ldd --version | head -n 1 && ~/.local/bin/encypher --version && ~/.local/bin/encypher verify /fixture/signed-test.jpg --json' | |
| - name: install.ps1 under Windows PowerShell 5.1 | |
| if: runner.os == 'Windows' | |
| shell: powershell | |
| env: | |
| ENCYPHER_CHECK_UNPINNED: ${{ !inputs.publish && matrix.unpinned && '1' || '' }} | |
| run: ./public/.github/scripts/install-check.ps1 | |
| - name: install.ps1 under PowerShell 7 | |
| if: runner.os == 'Windows' | |
| shell: pwsh | |
| env: | |
| ENCYPHER_INSTALL_DIR: ${{ runner.temp }}\pwsh7 | |
| run: ./public/.github/scripts/install-check.ps1 | |
| install-verdict: | |
| name: Install checks passed | |
| needs: [install-check] | |
| if: always() | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 5 | |
| steps: | |
| - name: Every install-check leg ran and passed | |
| env: | |
| RESULT: ${{ needs.install-check.result }} | |
| run: | | |
| if [[ "${RESULT}" != success ]]; then | |
| echo "install-check: ${RESULT}" >&2 | |
| exit 1 | |
| fi | |
| # The macOS end-to-end subset (client-core PRD, section 11), on a dry run | |
| # only: it spends live signatures and needs the built archives, not a | |
| # published release. The cases run from the installed archive with their | |
| # work directory on an exFAT volume; only case verdicts reach this public | |
| # log. It uses the same smoke key as install-check, so it starts after the | |
| # install legs (whatever their result) and the Macs run one at a time. | |
| macos-e2e: | |
| name: macOS end to end on ${{ matrix.name }} | |
| needs: [provenance, merge, install-check] | |
| if: ${{ !cancelled() && !inputs.publish && needs.merge.result == 'success' }} | |
| runs-on: ${{ matrix.os }} | |
| environment: build | |
| timeout-minutes: 120 | |
| strategy: | |
| fail-fast: false | |
| max-parallel: 1 | |
| matrix: | |
| include: | |
| - name: Apple Silicon | |
| os: macos-15 | |
| target: aarch64-apple-darwin | |
| - name: Intel | |
| os: macos-15-intel | |
| target: x86_64-apple-darwin | |
| env: | |
| VERSION: ${{ needs.provenance.outputs.version }} | |
| TARGET: ${{ matrix.target }} | |
| ENCYPHER_API_KEY: ${{ secrets.ENCYPHER_API_KEY }} | |
| steps: | |
| - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 | |
| with: | |
| repository: ${{ env.SOURCE_REPOSITORY }} | |
| ref: ${{ needs.provenance.outputs.revision }} | |
| ssh-key: ${{ secrets.ENCYPHER_SOURCE_DEPLOY_KEY }} | |
| path: source | |
| sparse-checkout: | | |
| packages/encypher-c2pa | |
| packaging | |
| persist-credentials: false | |
| - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 | |
| with: | |
| python-version: "3.12" | |
| # The artifact gate reads binaries with llvm-tools. The live matrix | |
| # builds its fixture generator offline against the engine's lock file, | |
| # so the crates are fetched first. | |
| - name: Install the pinned toolchain and llvm-tools, fetch the engine's crates | |
| working-directory: source/packages/encypher-c2pa | |
| run: | | |
| rustup toolchain install "${RUSTUP_TOOLCHAIN}" --profile minimal --component llvm-tools | |
| python ../../packaging/release/contained.py --label "cargo fetch" -- cargo fetch --locked | |
| - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 | |
| with: | |
| name: dist | |
| path: dist | |
| - name: c2patool 0.27.22 (pinned by SHA-256) | |
| run: | | |
| curl -fsSL -o "${RUNNER_TEMP}/c2patool.zip" \ | |
| https://github.com/contentauth/c2pa-rs/releases/download/c2patool-v0.27.22/c2patool-v0.27.22-universal-apple-darwin.zip | |
| echo "d064ca72e599c74e5fdc2ed04b47d6cb62210a2b14db589d078c2ec27c98d3df ${RUNNER_TEMP}/c2patool.zip" | shasum -a 256 -c | |
| unzip -q "${RUNNER_TEMP}/c2patool.zip" -d "${RUNNER_TEMP}/c2patool" | |
| tool="${RUNNER_TEMP}/c2patool/c2patool/c2patool" | |
| chmod +x "${tool}" | |
| "${tool}" --version | |
| echo "C2PATOOL=${tool}" >> "$GITHUB_ENV" | |
| - name: exFAT volume | |
| run: | | |
| hdiutil create -type SPARSE -size 6g -fs ExFAT -volname E2E "${RUNNER_TEMP}/e2e" | |
| hdiutil attach -nobrowse "${RUNNER_TEMP}/e2e.sparseimage" -mountpoint "${RUNNER_TEMP}/exfat" | |
| mount | grep -F "${RUNNER_TEMP}/exfat" | grep -qi exfat | |
| echo "EXFAT=${RUNNER_TEMP}/exfat" >> "$GITHUB_ENV" | |
| - name: Gate inventory of the archive under test | |
| run: | | |
| archive="dist/encypher-${VERSION}-${TARGET}.tar.gz" | |
| python source/packages/encypher-c2pa/scripts/check-client-artifact.py "${archive}" > "${RUNNER_TEMP}/gate.json" 2>/dev/null | |
| python -c 'import json, sys; print(json.dumps({"shipped": json.load(open(sys.argv[1]))}))' "${RUNNER_TEMP}/gate.json" > "${RUNNER_TEMP}/gate-inventory.json" | |
| echo "ARCHIVE=${GITHUB_WORKSPACE}/${archive}" >> "$GITHUB_ENV" | |
| # JPEG sign and verify, JSON-LD metadata, the layered TIFF over 300 MB, | |
| # the batch interrupted and resumed, the lid closed during finalize | |
| # (SIGSTOP) and the interrupted publish, all from the installed archive | |
| # with the work directory on exFAT. Only the `e2e:` verdicts are shown. | |
| - name: macOS end-to-end subset on exFAT | |
| run: | | |
| python source/packaging/release/contained.py --label "e2e: macOS subset" --show '^e2e: ' -- \ | |
| python source/packaging/release/macos_e2e.py --tarball "${ARCHIVE}" --work "${EXFAT}/macos" \ | |
| --c2patool "${C2PATOOL}" --gate-inventory "${RUNNER_TEMP}/gate-inventory.json" \ | |
| --fixture-cache "${RUNNER_TEMP}/fixtures" | |
| - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 | |
| if: failure() | |
| with: | |
| name: withheld-logs-macos-e2e-${{ matrix.target }} | |
| path: ${{ runner.temp }}/contained/*.log.cms | |
| if-no-files-found: ignore | |
| retention-days: 7 | |
| promote: | |
| name: Mark v${{ needs.provenance.outputs.version }} latest | |
| needs: [provenance, install-check] | |
| if: inputs.publish && needs.install-check.result == 'success' | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 10 | |
| permissions: | |
| contents: write | |
| steps: | |
| - name: Promote, then install the latest release unpinned | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| VERSION: ${{ needs.provenance.outputs.version }} | |
| run: | | |
| gh release edit "v${VERSION}" --repo "${GITHUB_REPOSITORY}" --prerelease=false --latest | |
| curl -fsSL "https://github.com/${GITHUB_REPOSITORY}/releases/latest/download/install.sh" \ | |
| | ENCYPHER_INSTALL_DIR="${RUNNER_TEMP}/latest" sh | |
| "${RUNNER_TEMP}/latest/encypher" --version | grep -qF "\"client\": \"${VERSION}\"" |