[
  {
    "path": ".envrc",
    "content": "use flake\n"
  },
  {
    "path": ".github/workflows/build.yaml",
    "content": "name: Release\r\n\r\non:\r\n  workflow_dispatch:\r\n\r\npermissions:\r\n  contents: write\r\n\r\njobs:\r\n  build:\r\n    runs-on: ubuntu-latest\r\n\r\n    steps:\r\n      - uses: actions/checkout@v4\r\n\r\n      - name: Set up java\r\n        uses: actions/setup-java@v4\r\n        with:\r\n          distribution: 'zulu' # See 'Supported distributions' for available options\r\n          java-version: '17'\r\n\r\n      - name: Install uv\r\n        uses: astral-sh/setup-uv@v7\r\n        with:\r\n          python-version: \"3.13\"\r\n          enable-cache: true\r\n\r\n      - name: Install dependencies\r\n        run: uv sync --frozen\r\n\r\n      - name: Try building\r\n        env:\r\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\r\n          TG_TOKEN: ${{ secrets.TG_TOKEN }}\r\n          TG_CHAT_ID: ${{ secrets.TG_CHAT_ID }}\r\n          TG_THREAD_ID: ${{ secrets.TG_THREAD_ID }}\r\n        run: |\r\n          mkdir -p bins\r\n          uv run main.py\r\n"
  },
  {
    "path": ".github/workflows/daily.yaml",
    "content": "name: Release - Daily\r\n\r\non:\r\n  schedule:\r\n    - cron: '0 0 * * *' # daily at 00:00 UTC\r\n  workflow_dispatch:\r\n\r\npermissions:\r\n  contents: write\r\n\r\njobs:\r\n  build:\r\n    runs-on: ubuntu-latest\r\n\r\n    steps:\r\n      - uses: actions/checkout@v4\r\n\r\n      - name: Set up java\r\n        uses: actions/setup-java@v4\r\n        with:\r\n          distribution: 'zulu' # See 'Supported distributions' for available options\r\n          java-version: '17'\r\n\r\n      - name: Install uv\r\n        uses: astral-sh/setup-uv@v7\r\n        with:\r\n          python-version: \"3.13\"\r\n          enable-cache: true\r\n\r\n      - name: Install dependencies\r\n        run: uv sync --frozen\r\n\r\n      - name: Try building\r\n        env:\r\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\r\n          TG_TOKEN: ${{ secrets.TG_TOKEN }}\r\n          TG_CHAT_ID: ${{ secrets.TG_CHAT_ID }}\r\n          TG_THREAD_ID: ${{ secrets.TG_THREAD_ID }}\r\n        run: |\r\n          mkdir -p bins\r\n          uv run main.py\r\n"
  },
  {
    "path": ".github/workflows/manual.yaml",
    "content": "name: Release - manual\n\non:\n  workflow_dispatch:\n    inputs:\n      version_input:\n        description: 'Enter the version'\n        required: true\n\npermissions:\n  contents: write\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n\n    steps:\n      - uses: actions/checkout@v4\n\n      - name: Set up java\n        uses: actions/setup-java@v4\n        with:\n          distribution: 'zulu' # See 'Supported distributions' for available options\n          java-version: '17'\n\n      - name: Install uv\n        uses: astral-sh/setup-uv@v7\n        with:\n          python-version: \"3.13\"\n          enable-cache: true\n\n      - name: Install dependencies\n        run: uv sync --frozen\n\n      - name: Try building\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n          TG_TOKEN: ${{ secrets.TG_TOKEN }}\n          TG_CHAT_ID: ${{ secrets.TG_CHAT_ID }}\n          TG_THREAD_ID: ${{ secrets.TG_THREAD_ID }}\n        run: |\n          mkdir -p bins\n          uv run main.py --m 1 --v ${{ github.event.inputs.version_input }}\n"
  },
  {
    "path": ".gitignore",
    "content": ".venv\n__pycache__\ntest.py\n.direnv\n"
  },
  {
    "path": "README.md",
    "content": "Apk builds of [piko](https://github.com/crimera/piko) patches\n\n# Credits\n- [revanced](https://github.com/ReVanced)\n- [@REAndroid's APKEditor](https://github.com/REAndroid/APKEditor) - Used in merging split apks\n- [j-hc](https://github.com/j-hc) - Project is inspired by j-hc's revanced builder template.\n"
  },
  {
    "path": "apkmirror.py",
    "content": "from dataclasses import dataclass\nfrom typing import cast\nfrom bs4 import BeautifulSoup, Tag\nfrom utils import download, get_scraper\n\n\n@dataclass\nclass Version:\n    version: str\n    link: str\n\n\n@dataclass\nclass Variant:\n    is_bundle: bool\n    link: str\n    architecture: str\n\n\n@dataclass\nclass App:\n    name: str\n    link: str\n\n\nclass FailedToFindElement(Exception):\n    def __init__(self, message=None) -> None:\n        self.message = (\n            f\"Failed to find element{' ' + message if message is not None else ''}\"  # noqa: E501\n        )\n        super().__init__(self.message)\n\n\nclass FailedToFetch(Exception):\n    def __init__(self, url=None) -> None:\n        self.message = f\"Failed to fetch{' ' + url if url is not None else ''}\"  # noqa: E501\n        super().__init__(self.message)\n\n\ndef get_versions(url: str) -> list[Version]:\n    \"\"\"Get the latest version of the app from the given apkmirror url\"\"\"\n    response = get_scraper().get(url)\n    if response.status_code != 200:\n        raise FailedToFetch(f\"{url}: {response.status_code}\")\n\n    bs4 = BeautifulSoup(response.text, \"html.parser\")\n    versions = bs4.find(\"div\", attrs={\"class\": \"listWidget\"})\n\n    out: list[Version] = []\n    if versions is not None:\n        for versionRow in cast(Tag, versions).findChildren(\"div\", recursive=False)[1:]:\n            if versionRow is None:\n                print(f\"{versionRow} is None\")\n                continue\n\n            version = versionRow.find(\"span\", {\"class\": \"infoSlide-value\"})\n            if version is None:\n                continue\n\n            version = version.string.strip()\n            link = f\"https://www.apkmirror.com/{versionRow.find('a')['href']}\"\n            out.append(Version(version=version, link=link))\n\n    return out\n\n\ndef download_apk(variant: Variant, path: str = \"big_file.apkm\"):\n    \"\"\"Download apk from the variant link\"\"\"\n    url = variant.link\n\n    response = get_scraper().get(url)\n\n    if response.status_code != 200:\n        raise FailedToFetch(url)\n\n    response_body = BeautifulSoup(response.content, \"html.parser\")\n\n    downloadButton = response_body.find(\"a\", {\"class\": \"downloadButton\"})\n    if downloadButton is None:\n        raise FailedToFindElement(\"Download button\")\n\n    download_page_link = (\n        f\"https://www.apkmirror.com/{cast(Tag, downloadButton).attrs['href']}\"\n    )\n\n    download_page = get_scraper().get(download_page_link)\n    if response.status_code != 200:\n        raise FailedToFetch(download_page_link)\n\n    download_page_body = BeautifulSoup(download_page.content, \"html.parser\")\n\n    direct_link = download_page_body.find(\"a\", {\"rel\": \"nofollow\"})\n    if direct_link is None:\n        raise FailedToFindElement(\"download link\")\n\n    direct_link_href = cast(Tag, direct_link).attrs[\"href\"]\n    direct_link_url = f\"https://www.apkmirror.com/{direct_link_href}\"\n    print(f\"Direct link: {direct_link_url}\")\n\n    download(\n        direct_link_url,\n        path,\n        use_scraper=True,\n        headers={\"Referer\": download_page_link},\n    )\n\n\ndef get_variants(version: Version) -> list[Variant]:\n    url = version.link\n    variants_page = get_scraper().get(url)\n    if variants_page is None:\n        raise FailedToFetch(url)\n\n    variants_page_body = BeautifulSoup(variants_page.content, \"html.parser\")\n\n    variants_table = variants_page_body.find(\"div\", {\"class\": \"table\"})\n    if variants_table is None:\n        raise FailedToFindElement(\"variants table\")\n\n    variants_table_rows = cast(Tag, variants_table).findChildren(\n        \"div\", recursive=False\n    )[1:]\n\n    variants: list[Variant] = []\n    for variant_row in variants_table_rows:\n        cells = variant_row.findChildren(\n            \"div\", {\"class\": \"table-cell\"}, recursive=False\n        )\n        if len(cells) == 0:\n            print(\"Could not find cells\")\n\n        is_bundle_tag = variant_row.find(\"span\", {\"class\": \"apkm-badge\"})\n        is_bundle = False\n        if is_bundle_tag is None:\n            print(\"Failed to find apk-badge\")\n        else:\n            is_bundle = is_bundle_tag.string.strip() == \"BUNDLE\"\n\n        architecture: str = cells[1].string\n        link_element = variant_row.find(\"a\", {\"class\": \"accent_color\"})\n        if link_element is None:\n            print(\"Failed to find the link element\")\n\n        link: str = f\"https://www.apkmirror.com{link_element.attrs['href']}\"\n        variants.append(\n            Variant(is_bundle=is_bundle, link=link, architecture=architecture)\n        )\n\n    print(variants)\n    return variants\n"
  },
  {
    "path": "build_variants.py",
    "content": "from apkmirror import Version\nfrom utils import patch_apk\n\n\ndef build_apks(latest_version: Version):\n    # patch\n    apk = \"big_file_merged.apk\"\n    patches = \"bins/patches.mpp\"\n    cli = \"bins/morphe-cli.jar\"\n\n    common_includes = [\n        \"Enable app downgrading\",\n        \"Hide FAB\",\n        \"Disable chirp font\",\n        \"Add ability to copy media link\",\n        \"Hide Banner\",\n        \"Hide promote button\",\n        \"Hide Community Notes\",\n        \"Delete from database\",\n        \"Customize Navigation Bar items\",\n        \"Remove premium upsell\",\n        \"Control video auto scroll\",\n        \"Force enable translate\",\n    ]\n\n    common_excludes = []\n\n    patch_apk(\n        cli,\n        patches,\n        apk,\n        includes=[\"Dynamic color\"] + common_includes,\n        excludes=common_excludes,\n        out=f\"x-piko-material-you-v{latest_version.version}.apk\",\n    )\n\n    patch_apk(\n        cli,\n        patches,\n        apk,\n        includes=common_includes,\n        excludes=[\"Dynamic color\"] + common_excludes,\n        out=f\"x-piko-v{latest_version.version}.apk\",\n    )\n\n    patch_apk(\n        cli,\n        patches,\n        apk,\n        includes=[\"Bring back twitter\", \"Dynamic color\"] + common_includes,\n        excludes=common_excludes,\n        out=f\"twitter-piko-material-you-v{latest_version.version}.apk\",\n    )\n\n    patch_apk(\n        cli,\n        patches,\n        apk,\n        includes=[\"Bring back twitter\"] + common_includes,\n        excludes=[\"Dynamic color\"] + common_excludes,\n        out=f\"twitter-piko-v{latest_version.version}.apk\",\n    )\n"
  },
  {
    "path": "constants.py",
    "content": "HEADERS = {\n    \"accept\": \"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7\",\n    \"accept-language\": \"en-GB,en;q=0.9\",\n    \"cache-control\": \"no-cache\",\n    \"pragma\": \"no-cache\",\n    \"priority\": \"u=0, i\",\n    \"sec-ch-ua\": '\"Not/A)Brand\";v=\"8\", \"Chromium\";v=\"126\"',\n    \"sec-ch-ua-mobile\": \"?0\",\n    \"sec-ch-ua-platform\": '\"Windows\"',\n    \"sec-fetch-dest\": \"document\",\n    \"sec-fetch-mode\": \"navigate\",\n    \"sec-fetch-site\": \"none\",\n    \"sec-fetch-user\": \"?1\",\n    \"upgrade-insecure-requests\": \"1\",\n    \"user-agent\": \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36\",\n}\n"
  },
  {
    "path": "download_bins.py",
    "content": "import re\n\nimport requests\n\nfrom utils import download\n\n\ndef download_release_asset(\n    repo: str,\n    regex: str,\n    out_dir: str,\n    filename = None,\n    include_prereleases: bool = False,\n    version = None,\n):\n    url = f\"https://api.github.com/repos/{repo}/releases\"\n\n    response = requests.get(url)\n    if response.status_code != 200:\n        raise Exception(\"Failed to fetch github\")\n\n    releases = [r for r in response.json() if include_prereleases or not r[\"prerelease\"]]\n\n    if not releases:\n        raise Exception(f\"No releases found for {repo}\")\n\n    if version is not None:\n        releases = [r for r in releases if r[\"tag_name\"] == version]\n\n    if not releases:\n        raise Exception(f\"No release found for version {version}\")\n\n    latest_release = releases[0]\n\n    link = None\n    for asset in latest_release[\"assets\"]:\n        name = asset[\"name\"]\n        if re.search(regex, name):\n            link = asset[\"browser_download_url\"]\n            if filename is None:\n                filename = name\n            break\n\n    if link is None:\n        raise Exception(f\"Failed to find asset matching {regex} on release {latest_release['tag_name']}\")\n\n    download(link, f\"{out_dir.lstrip('/')}/{filename}\")\n\n    return latest_release\n\n\ndef download_apkeditor():\n    print(\"Downloading APKEditor\")\n    download_release_asset(\"REAndroid/APKEditor\", \"APKEditor\", \"bins\", \"apkeditor.jar\")\n\n\ndef download_morphe_cli(include_prereleases: bool = False):\n    print(\"Downloading morphe cli\")\n    download_release_asset(\n        \"MorpheApp/morphe-cli\",\n        r\"^morphe-cli.*-all\\.jar$\",\n        \"bins\",\n        \"morphe-cli.jar\",\n        include_prereleases=include_prereleases,\n    )\n"
  },
  {
    "path": "flake.nix",
    "content": "{\n  description = \"A development environment for twitter-apk with Java\";\n\n  inputs = {\n    nixpkgs.url = \"github:NixOS/nixpkgs/nixos-unstable\";\n    flake-utils.url = \"github:numtide/flake-utils\";\n  };\n\n  outputs = { self, nixpkgs, flake-utils }:\n    flake-utils.lib.eachDefaultSystem (system:\n      let\n        pkgs = import nixpkgs { inherit system; };\n      in\n      {\n        devShells.default = pkgs.mkShell {\n          packages = with pkgs; [\n            jre          # Java Runtime Environment (OpenJDK)\n          ];\n\n          shellHook = ''\n            echo \"Environment loaded!\"\n            echo \"Java: $(java --version | head -n1)\"\n          '';\n        };\n      }\n    );\n}\n"
  },
  {
    "path": "github.py",
    "content": "import requests\nfrom constants import HEADERS\nfrom dataclasses import dataclass\n\n\n@dataclass\nclass Asset:\n    browser_download_url: str\n    name: str\n\n\n@dataclass\nclass GithubRelease:\n    tag_name: str\n    html_url: str\n    assets: list[Asset]\n\n\ndef get_last_build_version(repo_url: str) -> GithubRelease | None:\n    url = f\"https://api.github.com/repos/{repo_url}/releases/latest\"\n    response = requests.get(url, headers=HEADERS)\n\n    print(response.status_code)\n    if response.status_code == 200:\n        release = response.json()\n\n        assets = [\n            Asset(\n                browser_download_url=asset[\"browser_download_url\"], name=asset[\"name\"]\n            )\n            for asset in release[\"assets\"]\n        ]\n\n        return GithubRelease(\n            tag_name=release[\"tag_name\"], html_url=release[\"html_url\"], assets=assets\n        )\n    elif response.status_code == 404:\n        return\n"
  },
  {
    "path": "main.py",
    "content": "from apkmirror import Version, Variant\nfrom build_variants import build_apks\nfrom download_bins import download_apkeditor, download_morphe_cli, download_release_asset\nimport github\nfrom utils import panic, merge_apk, publish_release\nimport apkmirror\nimport os\nimport argparse\n\n\ndef get_latest_release(versions: list[Version]) -> Version | None:\n    for i in versions:\n        if i.version.find(\"release\") >= 0:\n            return i\n\n\ndef process(latest_version: Version):\n    variants: list[Variant] = apkmirror.get_variants(latest_version)\n\n    download_link: Variant | None = None\n    for variant in variants:\n        if variant.is_bundle and (\"universal\" in variant.architecture or \"arm64-v8a\" in variant.architecture):\n            download_link = variant\n            break\n\n    if download_link is None:\n        raise Exception(\"Bundle not Found\")\n\n    apkmirror.download_apk(download_link)\n    if not os.path.exists(\"big_file.apkm\"):\n        panic(\"Failed to download apk\")\n\n    download_apkeditor()\n\n    if not os.path.exists(\"big_file_merged.apk\"):\n        merge_apk(\"big_file.apkm\")\n    else:\n        print(\"apkm is already merged\")\n\n    download_morphe_cli(include_prereleases=True)\n\n    print(\"Downloading patches\")\n    pikoRelease = download_release_asset(\n        \"crimera/piko\", \"^patches.*mpp$\", \"bins\", \"patches.mpp\", include_prereleases=True\n    )\n\n    message: str = f\"\"\"\nChangelogs:\n[piko-{pikoRelease[\"tag_name\"]}]({pikoRelease[\"html_url\"]})\n\"\"\"\n\n    build_apks(latest_version)\n\n    publish_release(\n        latest_version.version,\n        [\n            f\"x-piko-v{latest_version.version}.apk\",\n            f\"x-piko-material-you-v{latest_version.version}.apk\",\n            f\"twitter-piko-v{latest_version.version}.apk\",\n            f\"twitter-piko-material-you-v{latest_version.version}.apk\",\n        ],\n        message,\n        latest_version.version\n    )\n\n\ndef main():\n    # get latest version\n    url: str = \"https://www.apkmirror.com/apk/x-corp/twitter/\"\n    repo_url: str = \"lluni/twitter-apk\"\n\n    versions = apkmirror.get_versions(url)\n\n    latest_version = get_latest_release(versions)\n    if latest_version is None:\n        raise Exception(\"Could not find the latest version\")\n\n    # only continue if it's a release\n    if latest_version.version.find(\"release\") < 0:\n        panic(\"Latest version is not a release version\")\n\n    last_build_version: github.GithubRelease | None = github.get_last_build_version(\n        repo_url\n    )\n\n    if last_build_version is None:\n        panic(\"Failed to fetch the latest build version\")\n        return\n\n    # Begin stuff\n    if last_build_version.tag_name != latest_version.version:\n        print(f\"New version found: {latest_version.version}\")\n    else:\n        print(\"No new version found\")\n        return\n\n    process(latest_version)\n\n\ndef manual(version:str):\n    link = f'https://www.apkmirror.com/apk/x-corp/twitter/x-{version.replace(\".\",\"-\")}-release'\n    latest_version = Version(link=link,version=version)\n    process(latest_version)\n\n\nif __name__ == \"__main__\":\n    parser = argparse.ArgumentParser(description='Piko APK')\n    # 0 = auto; 1 = manual;\n    parser.add_argument('--m', action=\"store\", dest='mode', default=0)\n    parser.add_argument('--v', action=\"store\", dest='version', default=0)\n\n    args = parser.parse_args()\n    mode = args.mode\n\n    if not mode: # auto\n        main()\n    else: # manual\n        version = args.version\n        if not version:\n            raise Exception(\"Version is required.\")\n        manual(version)\n"
  },
  {
    "path": "pyproject.toml",
    "content": "[project]\nname = \"twitter-apk\"\nversion = \"0.1.0\"\ndescription = \"Add your description here\"\nreadme = \"README.md\"\nrequires-python = \">=3.13\"\ndependencies = [\n    \"beautifulsoup4>=4.13.4\",\n    \"requests>=2.32.3\",\n    \"cloudscraper>=1.2.71\",\n]\n"
  },
  {
    "path": "utils.py",
    "content": "import os\nimport requests\nimport subprocess\nimport sys\nfrom typing import Optional, List\nfrom github import get_last_build_version\n\n_scraper = None\n\ndef get_scraper():\n    global _scraper\n    if _scraper is None:\n        import cloudscraper\n        _scraper = cloudscraper.create_scraper()\n        _scraper.headers.update({\n            \"User-Agent\": \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36\"\n        })\n    return _scraper\n\n\ndef panic(message: str):\n    print(message, file=sys.stderr)\n    exit(1)\n\n\ndef send_message(message: str, token: str, chat_id: str, thread_id: str):\n    endpoint = f\"https://api.telegram.org/bot{token}/sendMessage\"\n\n    data = {\n        \"parse_mode\": \"Markdown\",\n        \"disable_web_page_preview\": \"true\",\n        \"text\": message,\n        \"message_thread_id\": thread_id,\n        \"chat_id\": chat_id,\n    }\n\n    requests.post(endpoint, data=data)\n\n\ndef report_to_telegram():\n    tg_token = os.environ[\"TG_TOKEN\"]\n    tg_chat_id = os.environ[\"TG_CHAT_ID\"]\n    tg_thread_id = os.environ[\"TG_THREAD_ID\"]\n    release = get_last_build_version(\"crimera/twitter-apk\")\n\n    if release is None:\n        raise Exception(\"Could not fetch release\")\n\n    downloads = [\n        f\"[{asset.name}]({asset.browser_download_url})\" for asset in release.assets\n    ]\n\n    message = f\"\"\"\n[New Update Released !]({release.html_url})\n\n▼ Downloads ▼\n\n{\"\\n\\n\".join(downloads)}\n\"\"\"\n\n    print(message)\n\n    send_message(message, tg_token, tg_chat_id, tg_thread_id)\n\n\ndef download(link, out, headers=None, use_scraper=False):\n    dir_name = os.path.dirname(out)\n    if dir_name:\n        os.makedirs(dir_name, exist_ok=True)\n\n    if os.path.exists(out):\n        print(f\"{out} already exists skipping download\")\n        return\n\n    if use_scraper:\n        print(f\"Downloading with scraper: {link}\")\n\n    session = get_scraper() if use_scraper else requests\n\n    # https://www.slingacademy.com/article/python-requests-module-how-to-download-files-from-urls/#Streaming_Large_Files\n    with session.get(link, stream=True, headers=headers) as r:\n        r.raise_for_status()\n        with open(out, \"wb\") as f:\n            for chunk in r.iter_content(chunk_size=8192):\n                if chunk:\n                    f.write(chunk)\n\n\ndef run_command(command: list[str]):\n    cmd = subprocess.run(command, capture_output=True, shell=True)\n\n    try:\n        cmd.check_returncode()\n    except subprocess.CalledProcessError:\n        print(cmd.stdout)\n        print(cmd.stderr)\n        exit(1)\n\n\ndef merge_apk(path: str):\n    subprocess.run(\n        [\"java\", \"-jar\", \"./bins/apkeditor.jar\", \"m\", \"-extractNativeLibs\", \"true\", \"-i\", path]\n    ).check_returncode()\n\n\ndef patch_apk(\n    cli: str,\n    patches: str,\n    apk: str,\n    includes: list[str] | None = None,\n    excludes: list[str] | None = None,\n    out: str | None = None,\n):\n    command = [\n        \"java\",\n        \"-jar\",\n        cli,\n        \"patch\",\n        \"-p\",\n        patches,\n        # use j-hc's keystore so we wouldn't need to reinstall\n        \"--keystore\",\n        \"ks.keystore\",\n        \"--keystore-entry-password\",\n        \"123456789\",\n        \"--keystore-password\",\n        \"123456789\",\n        \"--signer\",\n        \"jhc\",\n        \"--keystore-entry-alias\",\n        \"jhc\",\n    ]\n\n    if includes is not None:\n        for i in includes:\n            command.append(\"-e\")\n            command.append(i)\n\n    if excludes is not None:\n        for e in excludes:\n            command.append(\"-d\")\n            command.append(e)\n\n    if out is not None:\n        command.append(\"--out\")\n        command.append(out)\n\n    command.append(apk)\n\n    subprocess.run(command).check_returncode()\n\n\ndef publish_release(tag: str, files: list[str], message: str, title = \"\"):\n    key = os.environ.get(\"GITHUB_TOKEN\")\n    if key is None:\n        raise Exception(\"GITHUB_TOKEN is not set\")\n\n    command = [\"gh\", \"release\", \"create\", \"--latest\", tag, \"--notes\", message, \"--title\", title]\n\n    if len(files) == 0:\n        raise Exception(\"Files should have atleast one item\")\n\n    for file in files:\n        command.append(file)\n\n    subprocess.run(command, env=os.environ.copy()).check_returncode()\n"
  }
]