name: Build sideloaded IPA from Release Assets on: workflow_dispatch: inputs: decrypted_instagram_url: description: "Decrypted Instagram IPA — direct URL" default: "" required: true type: string target: description: "Instagram version (410 needs a 410.x IPA above)" default: "latest" required: false type: choice options: - "latest" - "410" build_sideload: description: "Sideload IPA — with plugins" default: true required: false type: boolean build_noplugins: description: "No-plugins IPA — any sideloader / free account" default: false required: false type: boolean dup_bundle_id: description: "Bundle id (no-plugins only, blank = com.burbn.instagram)" default: "" required: false type: string dup_name: description: "App name on Home Screen (blank = Instagram)" default: "" required: false type: string release_tag: description: "Release tag (blank = latest)" default: "" required: false type: string upload_artifact: description: "Upload the built IPA" 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 env: DUP_ID: ${{ inputs.dup_bundle_id }} 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 if [ -n "$DUP_ID" ] && [ "${{ inputs.build_noplugins }}" != "true" ]; then echo "::error::A dup bundle id needs the no-plugins build. Enable 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" 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}" # 410 builds pull the RyukGram-410 deb and tag their output -410. SUF=""; [ "$INPUT_TARGET" = "410" ] && SUF="-410" echo "tag=${TAG}" >> "$GITHUB_OUTPUT" echo "version=${VERSION}" >> "$GITHUB_OUTPUT" echo "TAG=${TAG}" >> "$GITHUB_ENV" echo "VERSION=${VERSION}" >> "$GITHUB_ENV" echo "SUF=${SUF}" >> "$GITHUB_ENV" echo "Using release tag: ${TAG} (version ${VERSION}${SUF})" - name: Download release assets (rootless deb + injection dylibs) env: GH_TOKEN: ${{ github.token }} TARGET: ${{ inputs.target }} run: | set -euo pipefail mkdir -p packages release-assets # 410 tweak lives in the RyukGram-410 deb; main in RyukGram_. The # plugin dylibs are shared, so both targets pull the same ones. if [ "$TARGET" = "410" ]; then DEBP="RyukGram-410_*_rootless.deb"; else DEBP="RyukGram_*_rootless.deb"; fi gh release download "$TAG" --repo "$UPSTREAM_REPO" --dir release-assets --pattern "$DEBP" DEB="$(ls -t release-assets/$DEBP 2>/dev/null | head -n1 || true)" [ -n "$DEB" ] || { echo "::error::Rootless .deb ($DEBP) not found in release $TAG."; exit 1; } echo "DEB=${DEB}" >> "$GITHUB_ENV" IG_VER="$(basename "$DEB" | sed -nE 's/.*_IG(.*)_rootless\.deb$/\1/p')" [ -n "$IG_VER" ] || IG_VER=unknown echo "IG_VER=${IG_VER}" >> "$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 # The IPA is user-supplied, so its version can differ from the deb's. PLIST="$(unzip -Z1 packages/com.burbn.instagram.ipa 'Payload/*.app/Info.plist' 2>/dev/null | awk -F/ 'NF==3' | head -1 || true)" IPA_VER="" if [ -n "$PLIST" ]; then STAGE="$(mktemp -d)" unzip -qo -j packages/com.burbn.instagram.ipa "$PLIST" -d "$STAGE" IPA_VER="$(plutil -extract CFBundleShortVersionString raw -o - "$STAGE/Info.plist" 2>/dev/null || true)" rm -rf "$STAGE" fi if [ -n "$IPA_VER" ]; then echo "IG_VER=${IPA_VER}" >> "$GITHUB_ENV" echo "IG version from IPA: ${IPA_VER} (deb said ${IG_VER})" else echo "::warning::Could not read the version from the IPA; keeping IG_VER=${IG_VER}" fi ls -la packages - name: Build sideloaded IPA (cyan + ipapatch with zxPluginsInject) if: ${{ inputs.build_sideload }} env: DUP_NAME: ${{ inputs.dup_name }} run: | set -euo pipefail rm -f packages/RyukGram-sideloaded.ipa NAME_ARGS=() [ -n "$DUP_NAME" ] && NAME_ARGS=(-n "$DUP_NAME") 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 \ ${NAME_ARGS[@]+"${NAME_ARGS[@]}"} # 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_sideload_v${VERSION}_IG${IG_VER}.ipa" ls -la packages - name: Build no-plugins IPA (cyan + NoPluginsPatch, appex stripped) if: ${{ inputs.build_noplugins }} env: DUP_ID: ${{ inputs.dup_bundle_id }} DUP_NAME: ${{ inputs.dup_name }} run: | set -euo pipefail rm -f packages/RyukGram-noplugins.ipa DUP_ARGS=() [ -n "$DUP_ID" ] && DUP_ARGS+=(-b "$DUP_ID") [ -n "$DUP_NAME" ] && DUP_ARGS+=(-n "$DUP_NAME") 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 \ ${DUP_ARGS[@]+"${DUP_ARGS[@]}"} # 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" OUT="RyukGram_noplugins_v${VERSION}_IG${IG_VER}.ipa" [ -n "$DUP_ID" ] && OUT="RyukGram_noplugins_v${VERSION}_IG${IG_VER}-dup.ipa" mv packages/RyukGram-noplugins.ipa "packages/${OUT}" ls -la packages - name: Upload sideload IPA artifact if: ${{ inputs.upload_artifact && inputs.build_sideload }} uses: actions/upload-artifact@v4 with: name: RyukGram_sideload_v${{ steps.tag.outputs.version }} path: packages/RyukGram_sideload_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*.ipa if-no-files-found: error