name: Publish Snap after public release on: workflow_dispatch: inputs: tag: description: Existing public stable release tag to retry (for example v0.24.0) required: true type: string release: types: - published permissions: contents: read jobs: verify-snap: name: Verify public-release Snap assets if: ${{ (github.event_name == 'release' && startsWith(github.event.release.tag_name, 'v') && github.event.release.draft == false) || (github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/master') }} runs-on: ubuntu-latest timeout-minutes: 45 env: SOURCE_ARCHIVE_NAME: linux-frame-copy-runtime-sources.tar.xz outputs: receipt-sha256: ${{ steps.bind-transfer.outputs.receipt-sha256 }} steps: - name: Resolve public release id: resolve-release shell: bash env: GH_TOKEN: ${{ github.token }} REQUESTED_TAG: ${{ inputs.tag || github.event.release.tag_name }} EVENT_RELEASE_ID: ${{ github.event.release.id }} run: | set -euo pipefail [[ "${REQUESTED_TAG}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]] RELEASE_JSON="${RUNNER_TEMP}/snap-public-release.json" gh api "repos/${GITHUB_REPOSITORY}/releases/tags/${REQUESTED_TAG}" > "${RELEASE_JSON}" /usr/bin/jq --exit-status --arg tag "${REQUESTED_TAG}" ' .tag_name == $tag and .draft == false and .prerelease == false and (.published_at | type == "string" and length > 0) and (.id | type == "number" and . > 0 and . == floor) ' "${RELEASE_JSON}" > /dev/null RELEASE_ID="$(/usr/bin/jq --raw-output '.id' "${RELEASE_JSON}")" if [[ -n "${EVENT_RELEASE_ID}" ]]; then test "${RELEASE_ID}" = "${EVENT_RELEASE_ID}" fi printf 'tag=%s\nrelease-id=%s\n' "${REQUESTED_TAG}" "${RELEASE_ID}" >> "${GITHUB_OUTPUT}" - name: Checkout released tooling uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 with: ref: refs/tags/${{ steps.resolve-release.outputs.tag }} persist-credentials: false - name: Install release source verifier shell: bash run: | set -euo pipefail sudo apt-get update sudo apt-get install --no-install-recommends -y \ binutils \ squashfs-tools \ xz-utils - name: Select exact public release assets shell: bash env: GH_TOKEN: ${{ github.token }} RELEASE_ID: ${{ steps.resolve-release.outputs.release-id }} run: | set -euo pipefail gh api \ --paginate \ --slurp \ "repos/${GITHUB_REPOSITORY}/releases/${RELEASE_ID}/assets?per_page=100" \ > "${RUNNER_TEMP}/snap-release-assets.json" node tools/packaging/release-snap-assets.cjs select \ --assets-json "${RUNNER_TEMP}/snap-release-assets.json" \ --output-json "${RUNNER_TEMP}/selected-snap-release-assets.json" - name: Download exact public release assets shell: bash env: GH_TOKEN: ${{ github.token }} run: | set -euo pipefail ASSET_DIRECTORY="${RUNNER_TEMP}/snap-release-downloads" rm -rf "${ASSET_DIRECTORY}" mkdir -p "${ASSET_DIRECTORY}" node -e \ "const fs=require('node:fs'); const selected=JSON.parse(fs.readFileSync(process.argv[1],'utf8')); for (const asset of [...selected.snapAssets, selected.sourceAsset]) console.log([asset.id, asset.name].join('\\t'));" \ "${RUNNER_TEMP}/selected-snap-release-assets.json" | while IFS=$'\t' read -r ASSET_ID ASSET_NAME; do gh api \ --header "Accept: application/octet-stream" \ "repos/${GITHUB_REPOSITORY}/releases/assets/${ASSET_ID}" \ > "${ASSET_DIRECTORY}/${ASSET_NAME}" done - name: Verify downloaded public release assets shell: bash run: | set -euo pipefail VERIFIED_ASSET_STAGING="${RUNNER_TEMP}/verified-snap-release-assets" SEALED_ASSET_PARENT="/var/lib/iptvnator-snap-release" SEALED_ASSET_DIRECTORY="${SEALED_ASSET_PARENT}/assets" test -s "${RUNNER_TEMP}/snap-release-downloads/${SOURCE_ARCHIVE_NAME}" test ! -e "${VERIFIED_ASSET_STAGING}" sudo test ! -e "${SEALED_ASSET_PARENT}" node tools/packaging/release-snap-assets.cjs verify \ --manifest "${RUNNER_TEMP}/selected-snap-release-assets.json" \ --directory "${RUNNER_TEMP}/snap-release-downloads" \ --repository-revision "$(git rev-parse HEAD)" \ --verified-directory "${VERIFIED_ASSET_STAGING}" sudo install -d -m 0700 -o root -g root "${SEALED_ASSET_PARENT}" sudo mv "${VERIFIED_ASSET_STAGING}" "${SEALED_ASSET_DIRECTORY}" sudo chown -R root:root "${SEALED_ASSET_DIRECTORY}" sudo find "${SEALED_ASSET_DIRECTORY}" -type d -exec chmod 0555 {} + sudo find "${SEALED_ASSET_DIRECTORY}" -type f -exec chmod 0444 {} + sudo chmod 0555 "${SEALED_ASSET_PARENT}" - name: Reverify sealed public release assets shell: bash run: | set -euo pipefail VERIFIED_ASSET_DIRECTORY="/var/lib/iptvnator-snap-release/assets" node tools/packaging/release-snap-assets.cjs verify-sealed \ --manifest "${RUNNER_TEMP}/selected-snap-release-assets.json" \ --directory "${VERIFIED_ASSET_DIRECTORY}" \ --receipt "${VERIFIED_ASSET_DIRECTORY}/verified-release-assets.json" \ --repository-revision "$(git rev-parse HEAD)" - name: Bind verified release transfer id: bind-transfer shell: bash run: | set -euo pipefail RECEIPT_PATH="/var/lib/iptvnator-snap-release/assets/verified-release-assets.json" RECEIPT_RECORD="$(/usr/bin/sha256sum --binary "${RECEIPT_PATH}")" RECEIPT_SHA256="${RECEIPT_RECORD%% *}" [[ "${RECEIPT_SHA256}" =~ ^[a-f0-9]{64}$ ]] printf 'receipt-sha256=%s\n' "${RECEIPT_SHA256}" >> "${GITHUB_OUTPUT}" - name: Transfer verified release assets uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a with: name: verified-snap-release-assets path: /var/lib/iptvnator-snap-release/assets if-no-files-found: error retention-days: 1 compression-level: 0 include-hidden-files: true publish-snap: name: Publish verified public-release Snap to edge needs: verify-snap if: ${{ needs.verify-snap.result == 'success' }} runs-on: ubuntu-latest timeout-minutes: 20 steps: - name: Download verified release assets uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c with: name: verified-snap-release-assets path: ${{ runner.temp }}/verified-snap-release-assets - name: Seal transferred public release assets shell: bash env: EXPECTED_RECEIPT_SHA256: ${{ needs.verify-snap.outputs.receipt-sha256 }} run: | set -euo pipefail TRANSFERRED_ASSET_DIRECTORY="${RUNNER_TEMP}/verified-snap-release-assets" SEALED_ASSET_PARENT="/var/lib/iptvnator-snap-release" SEALED_ASSET_DIRECTORY="${SEALED_ASSET_PARENT}/assets" test -d "${TRANSFERRED_ASSET_DIRECTORY}" test ! -L "${TRANSFERRED_ASSET_DIRECTORY}" shopt -s nullglob dotglob TRANSFERRED_FILES=("${TRANSFERRED_ASSET_DIRECTORY}"/*) TRANSFERRED_SNAPS=("${TRANSFERRED_ASSET_DIRECTORY}"/*.snap) test "${#TRANSFERRED_SNAPS[@]}" -gt 0 test "${#TRANSFERRED_FILES[@]}" -eq "$(( ${#TRANSFERRED_SNAPS[@]} + 2 ))" test -f "${TRANSFERRED_ASSET_DIRECTORY}/linux-frame-copy-runtime-sources.tar.xz" test ! -L "${TRANSFERRED_ASSET_DIRECTORY}/linux-frame-copy-runtime-sources.tar.xz" test -f "${TRANSFERRED_ASSET_DIRECTORY}/verified-release-assets.json" test ! -L "${TRANSFERRED_ASSET_DIRECTORY}/verified-release-assets.json" for ASSET_FILE in "${TRANSFERRED_FILES[@]}"; do test -f "${ASSET_FILE}" test ! -L "${ASSET_FILE}" done RECEIPT_PATH="${TRANSFERRED_ASSET_DIRECTORY}/verified-release-assets.json" RECEIPT_RECORD="$(/usr/bin/sha256sum --binary "${RECEIPT_PATH}")" ACTUAL_RECEIPT_SHA256="${RECEIPT_RECORD%% *}" [[ "${EXPECTED_RECEIPT_SHA256}" =~ ^[a-f0-9]{64}$ ]] test "${ACTUAL_RECEIPT_SHA256}" = "${EXPECTED_RECEIPT_SHA256}" /usr/bin/jq --exit-status ' type == "object" and (keys == ["assets", "repositoryRevision", "schemaVersion"]) and (.schemaVersion == 1) and (.repositoryRevision | type == "string" and test("^[a-f0-9]{40,64}$")) and (.assets | type == "array" and length >= 2) and (.assets | all(.[]; type == "object" and (keys == ["id", "name", "sha256", "size"]) and (.id | type == "number" and . > 0 and . <= 9007199254740991 and . == floor) and (.name | type == "string" and length > 0 and . != "." and . != ".." and (contains("/") | not) and (contains("\\") | not) and (explode | all(.[]; . > 31 and . != 127))) and (.sha256 | type == "string" and test("^[a-f0-9]{64}$")) and (.size | type == "number" and . > 0 and . <= 9007199254740991 and . == floor))) and ([.assets[].name] | length == (unique | length)) and ([.assets[] | select(.name == "linux-frame-copy-runtime-sources.tar.xz")] | length == 1) and ([.assets[] | select(.name | endswith(".snap"))] | length >= 1) and (.assets | all(.[]; .name == "linux-frame-copy-runtime-sources.tar.xz" or (.name | endswith(".snap")))) ' "${RECEIPT_PATH}" > /dev/null RECEIPT_ASSET_COUNT="$(/usr/bin/jq --raw-output '.assets | length' "${RECEIPT_PATH}")" test "${RECEIPT_ASSET_COUNT}" -eq "$(( ${#TRANSFERRED_SNAPS[@]} + 1 ))" SIZE_MANIFEST="${RUNNER_TEMP}/verified-release-asset-sizes.tsv" CHECKSUM_MANIFEST="${RUNNER_TEMP}/verified-release-asset-checksums.txt" umask 077 /usr/bin/jq --raw-output \ '.assets[] | [.name, (.size | tostring)] | @tsv' \ "${RECEIPT_PATH}" > "${SIZE_MANIFEST}" while IFS=$'\t' read -r ASSET_NAME EXPECTED_SIZE; do ASSET_PATH="${TRANSFERRED_ASSET_DIRECTORY}/${ASSET_NAME}" ACTUAL_SIZE="$(/usr/bin/stat --format=%s -- "${ASSET_PATH}")" test "${ACTUAL_SIZE}" = "${EXPECTED_SIZE}" done < "${SIZE_MANIFEST}" /usr/bin/jq --raw-output \ '.assets[] | "\(.sha256) \(.name)"' \ "${RECEIPT_PATH}" > "${CHECKSUM_MANIFEST}" ( cd "${TRANSFERRED_ASSET_DIRECTORY}" /usr/bin/sha256sum --strict --check "${CHECKSUM_MANIFEST}" ) rm -f "${SIZE_MANIFEST}" "${CHECKSUM_MANIFEST}" shopt -u nullglob dotglob sudo test ! -e "${SEALED_ASSET_PARENT}" sudo install -d -m 0700 -o root -g root "${SEALED_ASSET_PARENT}" sudo mv "${TRANSFERRED_ASSET_DIRECTORY}" "${SEALED_ASSET_DIRECTORY}" sudo chown -R root:root "${SEALED_ASSET_DIRECTORY}" sudo find "${SEALED_ASSET_DIRECTORY}" -type d -exec chmod 0555 {} + sudo find "${SEALED_ASSET_DIRECTORY}" -type f -exec chmod 0444 {} + sudo chmod 0555 "${SEALED_ASSET_PARENT}" - name: Prepare Snapcraft upload workspace shell: bash run: | set -euo pipefail VERIFIED_ASSET_DIRECTORY="/var/lib/iptvnator-snap-release/assets" UPLOAD_DIRECTORY="/var/lib/iptvnator-snap-upload" sudo test ! -e "${UPLOAD_DIRECTORY}" sudo install -d -m 0700 -o root -g root "${UPLOAD_DIRECTORY}" shopt -s nullglob dotglob SNAP_FILES=("${VERIFIED_ASSET_DIRECTORY}"/*.snap) test "${#SNAP_FILES[@]}" -gt 0 for SNAP_FILE in "${SNAP_FILES[@]}"; do sudo ln -- "${SNAP_FILE}" "${UPLOAD_DIRECTORY}/${SNAP_FILE##*/}" done # Snapcraft extracts metadata beside the input file. Root-owned # hard links remain read-only; the sticky bit prevents replacement. sudo chmod 1777 "${UPLOAD_DIRECTORY}" shopt -u nullglob dotglob - name: Install Snapcraft shell: bash run: | set -euo pipefail sudo snap install snapcraft --classic --channel=stable - name: Publish all public-release snaps to edge shell: bash env: SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.snapcraft_token }} run: | set -euo pipefail VERIFIED_ASSET_DIRECTORY="/var/lib/iptvnator-snap-release/assets" UPLOAD_DIRECTORY="/var/lib/iptvnator-snap-upload" STORE_CREDENTIALS="${SNAPCRAFT_STORE_CREDENTIALS}" unset SNAPCRAFT_STORE_CREDENTIALS shopt -s nullglob dotglob SNAP_FILES=("${VERIFIED_ASSET_DIRECTORY}"/*.snap) test "${#SNAP_FILES[@]}" -gt 0 for SNAP_FILE in "${SNAP_FILES[@]}"; do SNAP_NAME="${SNAP_FILE##*/}" echo "Publishing public release asset: ${SNAP_NAME}" # Candidate/stable promotion is manual after installed-Snap frame-copy and missing-runtime fallback smoke. # GitHub Actions never promotes automatically. SNAPCRAFT_STORE_CREDENTIALS="${STORE_CREDENTIALS}" /snap/bin/snapcraft upload --release=edge "${UPLOAD_DIRECTORY}/${SNAP_NAME}" done unset STORE_CREDENTIALS shopt -u nullglob dotglob