name: Build sideloaded IPA from Release Assets on: workflow_dispatch: inputs: build_sideload: description: "Build normal sideload IPA (zxPluginsInject + ipapatch)" default: true required: false type: boolean build_noplugins: description: "Build no-plugins IPA (NoPluginsPatch, any sideloader)" default: false required: false type: boolean decrypted_instagram_url: description: "Direct URL to the decrypted Instagram IPA (must match the target — a 410.x IPA for IG 410)" default: "" required: true type: string target: description: "IG target (410 auto-picks the latest -410 release)" default: "434" required: false type: choice options: - "434" - "410" release_tag: description: "Release tag to pull assets from (leave empty for latest of target, e.g. v1.3.3-410)" default: "" required: false type: string upload_artifact: description: "Upload artifact" default: true required: false type: boolean concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true env: UPSTREAM_REPO: faroukbmiled/RyukGram jobs: build: name: Build RyukGram from Release runs-on: macos-latest permissions: contents: read steps: - name: Validate selection run: | if [ "${{ inputs.build_sideload }}" != "true" ] && \ [ "${{ inputs.build_noplugins }}" != "true" ]; then echo "::error::Select at least one of build_sideload / build_noplugins." exit 1 fi - name: Checkout uses: actions/checkout@v4 - name: Install Dependencies run: brew install dpkg - name: Install IPA tooling (cyan + ipapatch) run: | pip install --force-reinstall https://github.com/asdfzxcvbn/pyzule-rw/archive/main.zip mkdir -p "$HOME/.local/bin" curl -fLo "$HOME/.local/bin/ipapatch" https://github.com/asdfzxcvbn/ipapatch/releases/download/v2.1.3/ipapatch.macos-arm64 chmod +x "$HOME/.local/bin/ipapatch" echo "$HOME/.local/bin" >> "$GITHUB_PATH" - name: Resolve release tag id: tag env: GH_TOKEN: ${{ github.token }} INPUT_TAG: ${{ inputs.release_tag }} INPUT_TARGET: ${{ inputs.target }} run: | set -euo pipefail if [ -n "$INPUT_TAG" ]; then TAG="$INPUT_TAG" elif [ "$INPUT_TARGET" = "410" ]; then # Newest release whose tag ends in -410. TAG="$(gh release list --repo "$UPSTREAM_REPO" --limit 50 --json tagName -q '[.[].tagName | select(endswith("-410"))][0] // empty')" [ -n "$TAG" ] || { echo "::error::No -410 release found on $UPSTREAM_REPO."; exit 1; } else TAG="$(gh release view --repo "$UPSTREAM_REPO" --json tagName -q .tagName)" fi [ -n "$TAG" ] || { echo "::error::Could not resolve release tag."; exit 1; } VERSION="${TAG#v}" echo "tag=${TAG}" >> "$GITHUB_OUTPUT" echo "version=${VERSION}" >> "$GITHUB_OUTPUT" echo "TAG=${TAG}" >> "$GITHUB_ENV" echo "VERSION=${VERSION}" >> "$GITHUB_ENV" echo "Using release tag: ${TAG} (version ${VERSION})" - name: Download release assets (rootless deb + injection dylibs) env: GH_TOKEN: ${{ github.token }} run: | set -euo pipefail mkdir -p packages release-assets gh release download "$TAG" --repo "$UPSTREAM_REPO" --dir release-assets \ --pattern "RyukGram_*_rootless.deb" DEB="$(ls -t release-assets/RyukGram_*_rootless.deb 2>/dev/null | head -n1 || true)" [ -n "$DEB" ] || { echo "::error::Rootless .deb not found in release $TAG."; exit 1; } echo "DEB=${DEB}" >> "$GITHUB_ENV" if [ "${{ inputs.build_sideload }}" = "true" ]; then gh release download "$TAG" --repo "$UPSTREAM_REPO" --dir release-assets \ --pattern "zxPluginsInject_v*.dylib" ZX_DYLIB="$(ls -t release-assets/zxPluginsInject_v*.dylib 2>/dev/null | head -n1 || true)" [ -n "$ZX_DYLIB" ] || { echo "::error::zxPluginsInject dylib not found in release $TAG (enable include_zxinject when releasing)."; exit 1; } echo "ZX_DYLIB=${ZX_DYLIB}" >> "$GITHUB_ENV" fi if [ "${{ inputs.build_noplugins }}" = "true" ]; then gh release download "$TAG" --repo "$UPSTREAM_REPO" --dir release-assets \ --pattern "NoPluginsPatch_v*.dylib" NP_DYLIB="$(ls -t release-assets/NoPluginsPatch_v*.dylib 2>/dev/null | head -n1 || true)" [ -n "$NP_DYLIB" ] || { echo "::error::NoPluginsPatch dylib not found in release $TAG (enable include_noplugins_patch when releasing)."; exit 1; } echo "NP_DYLIB=${NP_DYLIB}" >> "$GITHUB_ENV" fi ls -la release-assets - name: Extract dylib + bundle from rootless deb run: | set -euo pipefail STAGE="$(mktemp -d)" dpkg-deb -x "$DEB" "$STAGE" DYLIB_SRC="$(find "$STAGE" -type f -name 'RyukGram.dylib' | head -1)" BUNDLE_SRC="$(find "$STAGE" -type d -name 'RyukGram.bundle' | head -1)" [ -n "$DYLIB_SRC" ] || { echo "::error::RyukGram.dylib not found in deb."; exit 1; } [ -n "$BUNDLE_SRC" ] || { echo "::error::RyukGram.bundle not found in deb."; exit 1; } cp "$DYLIB_SRC" packages/RyukGram.dylib rm -rf packages/RyukGram.bundle cp -R "$BUNDLE_SRC" packages/RyukGram.bundle if [ "${{ inputs.build_sideload }}" = "true" ]; then # Match the @rpath LC that ipapatch writes into target binaries. cp "$ZX_DYLIB" packages/zxPluginsInject.dylib install_name_tool -id "@rpath/zxPluginsInject.dylib" packages/zxPluginsInject.dylib 2>/dev/null || true fi if [ "${{ inputs.build_noplugins }}" = "true" ]; then cp "$NP_DYLIB" packages/NoPluginsPatch.dylib fi rm -rf "$STAGE" ls -la packages ls -la packages/RyukGram.bundle | head -20 - name: Prepare Instagram IPA env: Instagram_URL: ${{ inputs.decrypted_instagram_url }} run: | set -euo pipefail wget "$Instagram_URL" --no-verbose -O packages/com.burbn.instagram.ipa ls -la packages - name: Build sideloaded IPA (cyan + ipapatch with zxPluginsInject) if: ${{ inputs.build_sideload }} run: | set -euo pipefail rm -f packages/RyukGram-sideloaded.ipa cyan \ -i packages/com.burbn.instagram.ipa \ -o packages/RyukGram-sideloaded.ipa \ -f packages/RyukGram.dylib packages/RyukGram.bundle \ -c 9 -m 15.0 -du # Embed Safari "Open in Instagram" extension before ipapatch # re-signs, so instagram.com links open the app. APPEX_SRC="extensions/OpenInstagramSafariExtension.appex" if [ -d "$APPEX_SRC" ]; then echo "Embedding Safari extension" INJECT_TMP="$(mktemp -d)" unzip -q packages/RyukGram-sideloaded.ipa -d "$INJECT_TMP" APP_DIR="$(find "$INJECT_TMP/Payload" -maxdepth 1 -type d -name '*.app' | head -1)" if [ -n "$APP_DIR" ]; then mkdir -p "$APP_DIR/PlugIns" rm -rf "$APP_DIR/PlugIns/OpenInstagramSafariExtension.appex" cp -R "$APPEX_SRC" "$APP_DIR/PlugIns/" ( cd "$INJECT_TMP" && zip -qr -9 ../repacked.ipa Payload ) mv "$INJECT_TMP/../repacked.ipa" packages/RyukGram-sideloaded.ipa fi rm -rf "$INJECT_TMP" fi echo "Running ipapatch (zxPluginsInject LC injection)" ipapatch \ --input packages/RyukGram-sideloaded.ipa \ --inplace --noconfirm \ --dylib packages/zxPluginsInject.dylib mv packages/RyukGram-sideloaded.ipa "packages/RyukGram_sideloaded_v${VERSION}.ipa" ls -la packages - name: Build no-plugins IPA (cyan + NoPluginsPatch, appex stripped) if: ${{ inputs.build_noplugins }} run: | set -euo pipefail rm -f packages/RyukGram-noplugins.ipa cyan \ -i packages/com.burbn.instagram.ipa \ -o packages/RyukGram-noplugins.ipa \ -f packages/RyukGram.dylib packages/NoPluginsPatch.dylib packages/RyukGram.bundle \ -c 9 -m 15.0 -du # No-plugins: strip every app extension (no ipapatch, no Safari ext). INJECT_TMP="$(mktemp -d)" unzip -q packages/RyukGram-noplugins.ipa -d "$INJECT_TMP" APP_DIR="$(find "$INJECT_TMP/Payload" -maxdepth 1 -type d -name '*.app' | head -1)" if [ -n "$APP_DIR" ]; then find "$APP_DIR" -type d -name '*.appex' -prune -exec rm -rf {} + ( cd "$INJECT_TMP" && zip -qr -9 ../repacked.ipa Payload ) mv "$INJECT_TMP/../repacked.ipa" packages/RyukGram-noplugins.ipa fi rm -rf "$INJECT_TMP" mv packages/RyukGram-noplugins.ipa "packages/RyukGram_noplugins_v${VERSION}.ipa" ls -la packages - name: Upload sideload IPA artifact if: ${{ inputs.upload_artifact && inputs.build_sideload }} uses: actions/upload-artifact@v4 with: name: RyukGram_sideloaded_v${{ steps.tag.outputs.version }} path: packages/RyukGram_sideloaded_v*.ipa if-no-files-found: error - name: Upload no-plugins IPA artifact if: ${{ inputs.upload_artifact && inputs.build_noplugins }} uses: actions/upload-artifact@v4 with: name: RyukGram_noplugins_v${{ steps.tag.outputs.version }} path: packages/RyukGram_noplugins_v*.ipa if-no-files-found: error