├── .github ├── ISSUE_TEMPLATE.md ├── CODEOWNERS ├── workflows │ ├── pr.yml │ ├── repodata_patching.yml │ └── master.yml └── PULL_REQUEST_TEMPLATE.md ├── token_reset └── example.txt ├── broken └── example.txt ├── not_broken ├── example.txt ├── libxml2.9.11.txt └── libxml2.9.12.txt ├── environment.yml ├── README.md ├── update_repodata_patches.py ├── mark_broken.py └── token_reset.py /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @conda-forge/core 2 | -------------------------------------------------------------------------------- /token_reset/example.txt: -------------------------------------------------------------------------------- 1 | cf-autotick-bot-test-package 2 | -------------------------------------------------------------------------------- /broken/example.txt: -------------------------------------------------------------------------------- 1 | win-64/cf-autotick-bot-test-package-0.4-py38_0.tar.bz2 2 | win-64/cf-autotick-bot-test-package-0.4-py27_0.tar.bz2 3 | -------------------------------------------------------------------------------- /not_broken/example.txt: -------------------------------------------------------------------------------- 1 | win-64/cf-autotick-bot-test-package-0.4-py38_0.tar.bz2 2 | win-64/cf-autotick-bot-test-package-0.4-py27_0.tar.bz2 3 | -------------------------------------------------------------------------------- /environment.yml: -------------------------------------------------------------------------------- 1 | name: cf 2 | channels: 3 | - conda-forge 4 | - nodefaults 5 | dependencies: 6 | - anaconda-client 7 | - python=3.8 8 | - requests 9 | - conda-smithy 10 | - pygithub 11 | -------------------------------------------------------------------------------- /not_broken/libxml2.9.11.txt: -------------------------------------------------------------------------------- 1 | linux-ppc64le/libxml2-2.9.11-h5b1524f_0.tar.bz2 2 | win-64/libxml2-2.9.11-hf5bbc77_0.tar.bz2 3 | osx-64/libxml2-2.9.11-h93ec3fd_0.tar.bz2 4 | osx-arm64/libxml2-2.9.11-h538f51a_0.tar.bz2 5 | linux-64/libxml2-2.9.11-h72842e0_0.tar.bz2 6 | linux-aarch64/libxml2-2.9.11-hd674cf7_0.tar.bz2 -------------------------------------------------------------------------------- /.github/workflows/pr.yml: -------------------------------------------------------------------------------- 1 | name: check 2 | on: 3 | pull_request: 4 | branches: 5 | - main 6 | jobs: 7 | check: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v2 11 | 12 | - uses: conda-incubator/setup-miniconda@v2 13 | with: 14 | activate-environment: cf 15 | environment-file: environment.yml 16 | auto-activate-base: true 17 | miniforge-version: latest 18 | miniforge-variant: Mambaforge 19 | 20 | - name: Check packages exist on main (or label/broken) 21 | shell: bash -l {0} 22 | run: | 23 | conda activate cf 24 | python mark_broken.py check 25 | 26 | - name: Check feedstocks exist for token reset 27 | shell: bash -l {0} 28 | run: | 29 | conda activate cf 30 | python token_reset.py check 31 | -------------------------------------------------------------------------------- /not_broken/libxml2.9.12.txt: -------------------------------------------------------------------------------- 1 | win-64/libxml2-2.9.12-hf5bbc77_2.tar.bz2 2 | osx-64/libxml2-2.9.12-he03b247_2.tar.bz2 3 | linux-ppc64le/libxml2-2.9.12-hc8bd4e3_2.tar.bz2 4 | osx-arm64/libxml2-2.9.12-h97d9dda_2.tar.bz2 5 | linux-aarch64/libxml2-2.9.12-h370961a_2.tar.bz2 6 | linux-64/libxml2-2.9.12-h22db469_2.tar.bz2 7 | linux-aarch64/libxml2-2.9.12-h1e2ce75_1.tar.bz2 8 | linux-ppc64le/libxml2-2.9.12-h1876533_1.tar.bz2 9 | win-64/libxml2-2.9.12-hf5bbc77_1.tar.bz2 10 | osx-arm64/libxml2-2.9.12-hedbfbf4_1.tar.bz2 11 | osx-64/libxml2-2.9.12-h7e28ab6_1.tar.bz2 12 | linux-64/libxml2-2.9.12-h885dcf4_1.tar.bz2 13 | win-64/libxml2-2.9.12-hf5bbc77_0.tar.bz2 14 | linux-ppc64le/libxml2-2.9.12-h5b1524f_0.tar.bz2 15 | linux-aarch64/libxml2-2.9.12-hd674cf7_0.tar.bz2 16 | osx-64/libxml2-2.9.12-h93ec3fd_0.tar.bz2 17 | osx-arm64/libxml2-2.9.12-h538f51a_0.tar.bz2 18 | linux-64/libxml2-2.9.12-h72842e0_0.tar.bz2 -------------------------------------------------------------------------------- /.github/workflows/repodata_patching.yml: -------------------------------------------------------------------------------- 1 | name: repodata_patching 2 | 3 | on: 4 | schedule: 5 | - cron: '0 0 * * 0' 6 | workflow_dispatch: null 7 | 8 | jobs: 9 | repodata_patching: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Prevent multiple jobs running in parallel 13 | id: conversion_lock 14 | uses: beckermr/turnstyle-python@v1 15 | with: 16 | abort-after-seconds: 3 17 | poll-interval-seconds: 2 18 | github-token: ${{ secrets.GITHUB_TOKEN }} 19 | continue-on-error: true 20 | 21 | - uses: actions/checkout@v2 22 | # outcome is evaluated before continue-on-error above 23 | if: steps.conversion_lock.outcome == 'success' 24 | 25 | - uses: conda-incubator/setup-miniconda@v2 26 | if: steps.conversion_lock.outcome == 'success' 27 | with: 28 | activate-environment: cf 29 | environment-file: environment.yml 30 | auto-activate-base: true 31 | miniforge-version: latest 32 | miniforge-variant: Mambaforge 33 | 34 | - name: Generate token 35 | if: steps.conversion_lock.outcome == 'success' 36 | id: generate_token 37 | uses: tibdex/github-app-token@v1 38 | with: 39 | app_id: ${{ secrets.CF_CURATOR_APP_ID }} 40 | private_key: ${{ secrets.CF_CURATOR_PRIVATE_KEY }} 41 | 42 | - name: patch repodata 43 | if: steps.conversion_lock.outcome == 'success' 44 | shell: bash -l {0} 45 | run: | 46 | conda activate cf 47 | git config --global user.email "79913779+conda-forge-curator[bot]@users.noreply.github.com" 48 | git config --global user.name "conda-forge-curator[bot]" 49 | git config --global pull.rebase false 50 | python update_repodata_patches.py 51 | env: 52 | GITHUB_TOKEN: ${{ steps.generate_token.outputs.token }} 53 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 10 | 11 | Guidelines for marking packages as broken: 12 | 13 | * We prefer to patch the repo data (see [here](https://github.com/conda-forge/conda-forge-repodata-patches-feedstock)) 14 | instead of marking packages as broken. This alternative workflow makes environments more reproducible. 15 | * Packages with requirements/metadata that are too strict but otherwise work are 16 | not technically broken and should not be marked as such. 17 | * Packages with missing metadata can be marked as broken on a temporary basis 18 | but should be patched in the repo data and be marked unbroken later. 19 | * In some cases where the number of users of a package is small or it is used by 20 | the maintainers only, we can allow packages to be marked broken more liberally. 21 | * We (`conda-forge/core`) try to make a decision on these requests within 24 hours. 22 | 23 | What will happen when a package is marked broken? 24 | 25 | * Our bots will add the `broken` label to the package. The `main` label will remain on the package and this is normal. 26 | * Our bots will rebuild our repodata patches to remove this package from the repodata. 27 | * In a few hours after the `anaconda.org` CDN picks up the new patches, you will no longer be able to install the package from the `main` channel. 28 | 29 | Checklist: 30 | 31 | * [ ] Make sure your package is in the right spot (`broken/*` for adding the 32 | `broken` label, `not_broken/*` for removing the `broken` label, or `token_reset/*` 33 | for token resets) 34 | * [ ] Added a description of the problem with the package in the PR description. 35 | * [ ] Added links to any relevant issues/PRs in the PR description. 36 | * [ ] Pinged the team for the package for their input. 37 | 38 | 44 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # admin requests 2 | 3 | [![repodata_patching](https://github.com/conda-forge/admin-requests/actions/workflows/repodata_patching.yml/badge.svg)](https://github.com/conda-forge/admin-requests/actions/workflows/repodata_patching.yml) [![run](https://github.com/conda-forge/admin-requests/actions/workflows/master.yml/badge.svg)](https://github.com/conda-forge/admin-requests/actions/workflows/master.yml) 4 | 5 | This repo is for making requests to `conda-forge/core` for various administrative 6 | tasks. 7 | 8 | 9 | ## Mark packages as broken on conda-forge 10 | 11 | If you want to mark a package as broken on `conda-forge`, send a Pull Request 12 | adding a new `.txt` file in `broken/` with a list of the full names of the packages 13 | to which the `broken` label will be added. See `broken/example.txt` for an example. 14 | 15 | Guidelines for marking packages as broken: 16 | 17 | * If the package is functional but with incorrect metadata (e.g. missing dependencies), then 18 | we prefer to patch the repo data (see [here](https://github.com/conda-forge/conda-forge-repodata-patches-feedstock)) 19 | instead of marking packages as broken. This alternative workflow makes environments more reproducible. 20 | * Packages with requirements/metadata that are too strict but otherwise work are 21 | not technically broken and should not be marked as such. 22 | * Packages with missing metadata can be marked as broken on a temporary basis 23 | but should be patched in the repo data and be marked unbroken later. 24 | * In some cases where the number of users of a package is small or it is used by 25 | the maintainers only, we can allow packages to be marked broken more liberally. 26 | * We (`conda-forge/core`) try to make a decision on these requests within 24 hours. 27 | 28 | 29 | ## Mark packages as not broken on conda-forge 30 | 31 | If you want to remove the broken label from packages on `conda-forge`, send a Pull Request 32 | adding a new `.txt` file in `not_broken/` with a list of the full names of the packages 33 | for which the label `broken` will be removed. See `not_broken/example.txt` for an example. 34 | 35 | 36 | ## Reset your Feedstock Token 37 | 38 | If you want to reset your feedstock token to fix issues with uploads, place the name of your feedstock in 39 | a new `.txt` file in `token_reset/`. See `token_reset/example.txt` for an example. You should use the name 40 | without `-feedstock` (e.g., for `python-feedstock`, you put in just `python`). 41 | -------------------------------------------------------------------------------- /update_repodata_patches.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import subprocess 3 | import os 4 | import tempfile 5 | import github 6 | import datetime 7 | 8 | 9 | def _commit_to_patches(tmpdir): 10 | subprocess.check_call( 11 | "git reset --hard HEAD", 12 | cwd=os.path.join(tmpdir, "conda-forge-repodata-patches-feedstock"), 13 | shell=True, 14 | ) 15 | 16 | subprocess.check_call( 17 | "git commit --allow-empty -am 'resync repo data for weekly cron-job'", 18 | cwd=os.path.join(tmpdir, "conda-forge-repodata-patches-feedstock"), 19 | shell=True, 20 | ) 21 | 22 | subprocess.check_call( 23 | "git push", 24 | cwd=os.path.join(tmpdir, "conda-forge-repodata-patches-feedstock"), 25 | shell=True, 26 | ) 27 | 28 | 29 | def _post_issue_with_diff(diff): 30 | msg = """\ 31 | Hi! Our weekly job found a non-zero repodata patch diff: 32 | 33 |
34 | 35 | ``` 36 | %s 37 | ``` 38 | 39 |
40 | """ % diff 41 | 42 | dstr = datetime.date.today().strftime("%Y-%m-%d") 43 | gh = github.Github(os.environ['GITHUB_TOKEN']) 44 | repo = gh.get_repo("conda-forge/conda-forge-repodata-patches-feedstock") 45 | repo.create_issue( 46 | "[%s] non-zero repodata patch diff" % dstr, 47 | body=msg, 48 | ) 49 | 50 | 51 | def update_repodata_patches(dry_run): 52 | with tempfile.TemporaryDirectory() as tmpdir: 53 | subprocess.check_call( 54 | "git clone https://github.com/conda-forge/" 55 | "conda-forge-repodata-patches-feedstock.git", 56 | cwd=tmpdir, 57 | shell=True, 58 | ) 59 | 60 | subprocess.check_call( 61 | "git remote set-url --push origin " 62 | "https://x-access-token:${GITHUB_TOKEN}@github.com/conda-forge/" 63 | "conda-forge-repodata-patches-feedstock.git", 64 | cwd=os.path.join(tmpdir, "conda-forge-repodata-patches-feedstock"), 65 | shell=True, 66 | ) 67 | 68 | d = subprocess.check_output( 69 | "python show_diff.py", 70 | cwd=os.path.join( 71 | tmpdir, 72 | "conda-forge-repodata-patches-feedstock", 73 | "recipe" 74 | ), 75 | shell=True, 76 | ).decode("utf-8") 77 | 78 | empty = True 79 | for line in d.splitlines(): 80 | line = line.strip() 81 | if len(line) > 0 and not line.startswith("Downloading"): 82 | empty = False 83 | 84 | print("diff:\n" + d, flush=True) 85 | print("is empty:", empty, flush=True) 86 | 87 | if len(d) > 0 and not empty: 88 | if not dry_run: 89 | _post_issue_with_diff(d) 90 | _commit_to_patches(tmpdir) 91 | 92 | 93 | if __name__ == "__main__": 94 | if len(sys.argv) > 2: 95 | raise RuntimeError("Need 0 or 1 arguments") 96 | if len(sys.argv) == 2 and sys.argv[1] == '--dry-run': 97 | dry_run = True 98 | else: 99 | dry_run = False 100 | 101 | update_repodata_patches(dry_run) 102 | -------------------------------------------------------------------------------- /.github/workflows/master.yml: -------------------------------------------------------------------------------- 1 | name: run 2 | on: 3 | push: 4 | branches: 5 | - main 6 | schedule: 7 | - cron: "0,15,30,45 * * * *" 8 | 9 | jobs: 10 | run: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Prevent multiple jobs running in parallel 14 | id: conversion_lock 15 | uses: beckermr/turnstyle-python@v1 16 | with: 17 | abort-after-seconds: 3 18 | poll-interval-seconds: 2 19 | github-token: ${{ secrets.GITHUB_TOKEN }} 20 | continue-on-error: true 21 | 22 | - uses: actions/checkout@v2 23 | # outcome is evaluated before continue-on-error above 24 | if: steps.conversion_lock.outcome == 'success' 25 | 26 | - name: fast finish 27 | if: ${{ steps.conversion_lock.outcome == 'success' }} 28 | run: | 29 | set -x 30 | # avoid wasting CI time if there is nothing to do 31 | broken_count="$(ls broken/*.txt | grep -v broken/example.txt | wc -l)" 32 | not_broken_count="$(ls not_broken/*.txt | grep -v not_broken/example.txt | wc -l)" 33 | token_reset_count="$(ls token_reset/*.txt | grep -v token_reset/example.txt | wc -l)" 34 | if [[ "${broken_count}" == "0" && "${not_broken_count}" == "0" && "${token_reset_count}" == "0" ]]; then 35 | echo "nothing to do, setting ci skip!" 36 | echo "CI_SKIP=true" >> $GITHUB_ENV 37 | fi 38 | 39 | - uses: conda-incubator/setup-miniconda@v2 40 | if: steps.conversion_lock.outcome == 'success' && ! env.CI_SKIP 41 | with: 42 | activate-environment: cf 43 | environment-file: environment.yml 44 | auto-activate-base: true 45 | miniforge-version: latest 46 | miniforge-variant: Mambaforge 47 | 48 | - name: Generate token 49 | if: steps.conversion_lock.outcome == 'success' && ! env.CI_SKIP 50 | id: generate_token 51 | uses: tibdex/github-app-token@v1 52 | with: 53 | app_id: ${{ secrets.CF_CURATOR_APP_ID }} 54 | private_key: ${{ secrets.CF_CURATOR_PRIVATE_KEY }} 55 | 56 | - name: mark packages as broken 57 | if: steps.conversion_lock.outcome == 'success' && ! env.CI_SKIP 58 | shell: bash -l {0} 59 | run: | 60 | conda activate cf 61 | git config --global user.email "79913779+conda-forge-curator[bot]@users.noreply.github.com" 62 | git config --global user.name "conda-forge-curator[bot]" 63 | git config --global pull.rebase false 64 | python mark_broken.py mark 65 | env: 66 | BINSTAR_TOKEN: ${{ secrets.PROD_BINSTAR_TOKEN }} 67 | GITHUB_TOKEN: ${{ steps.generate_token.outputs.token }} 68 | 69 | - name: reset feedstock tokens 70 | if: steps.conversion_lock.outcome == 'success' && ! env.CI_SKIP 71 | shell: bash -l {0} 72 | run: | 73 | conda activate cf 74 | git config --global user.email "79913779+conda-forge-curator[bot]@users.noreply.github.com" 75 | git config --global user.name "conda-forge-curator[bot]" 76 | git config --global pull.rebase false 77 | python token_reset.py reset 78 | env: 79 | STAGING_BINSTAR_TOKEN: ${{ secrets.STAGING_BINSTAR_TOKEN }} 80 | GITHUB_TOKEN: ${{ steps.generate_token.outputs.token }} 81 | CIRCLE_TOKEN: ${{ secrets.CIRCLE_TOKEN }} 82 | TRAVIS_TOKEN: ${{ secrets.ORGWIDE_TRAVIS_TOKEN }} 83 | AZURE_TOKEN: ${{ secrets.AZURE_TOKEN }} 84 | DRONE_TOKEN: ${{ secrets.DRONE_TOKEN }} 85 | 86 | - name: Push changes 87 | if: steps.conversion_lock.outcome == 'success' && ! env.CI_SKIP 88 | uses: ad-m/github-push-action@master 89 | with: 90 | github_token: ${{ secrets.GITHUB_TOKEN }} 91 | -------------------------------------------------------------------------------- /mark_broken.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from glob import glob 3 | import subprocess 4 | import os 5 | import tempfile 6 | import requests 7 | 8 | 9 | def split_pkg(pkg): 10 | if pkg.endswith(".tar.bz2"): 11 | pkg = pkg[:-len(".tar.bz2")] 12 | elif pkg.endswith(".conda"): 13 | pkg = pkg[:-len(".conda")] 14 | else: 15 | raise RuntimeError("Can only process packages that end in .tar.bz2 or .conda!") 16 | plat, pkg_name = pkg.split("/") 17 | name_ver, build = pkg_name.rsplit('-', 1) 18 | name, ver = name_ver.rsplit('-', 1) 19 | return plat, name, ver, build 20 | 21 | 22 | def get_broken_files(): 23 | return ( 24 | [f for f in glob("broken/*") if f != "broken/example.txt"] 25 | + [f for f in glob("pkgs/*") if f != "pkgs/example.txt"] 26 | ) 27 | 28 | 29 | def get_not_broken_files(): 30 | return [f for f in glob("not_broken/*") if f != "not_broken/example.txt"] 31 | 32 | 33 | def check_packages(): 34 | for channel, filenames in ( 35 | ("conda-forge", get_broken_files()), 36 | ("conda-forge/label/broken", get_not_broken_files()), 37 | ): 38 | for file_name in filenames: 39 | with open(file_name, "r") as f: 40 | pkgs = f.readlines() 41 | pkgs = [pkg.strip() for pkg in pkgs] 42 | for pkg in pkgs: 43 | # ignore blank lines or Python-style comments 44 | if pkg.startswith('#') or len(pkg) == 0: 45 | continue 46 | plat, name, ver, build = split_pkg(pkg) 47 | subprocess.check_call( 48 | f"CONDA_SUBDIR={plat} conda search {name}={ver}={build} " 49 | f"-c {channel} --override-channels", 50 | shell=True, 51 | ) 52 | 53 | 54 | def mark_broken_file(file_name): 55 | did_one = False 56 | 57 | with open(file_name, "r") as f: 58 | pkgs = f.readlines() 59 | pkgs = [pkg.strip() for pkg in pkgs] 60 | for pkg in pkgs: 61 | # ignore blank lines or Python-style comments 62 | if pkg.startswith('#') or len(pkg) == 0: 63 | continue 64 | print(" package: %s" % pkg, flush=True) 65 | plat, name, ver, build = split_pkg(pkg) 66 | r = requests.post( 67 | "https://api.anaconda.org/channels/conda-forge/broken", 68 | headers={'Authorization': 'token {}'.format(os.environ["BINSTAR_TOKEN"])}, 69 | json={ 70 | "basename": pkg, 71 | "package": name, 72 | "version": ver, 73 | } 74 | ) 75 | if r.status_code != 201: 76 | print(" could not mark broken", flush=True) 77 | return did_one 78 | else: 79 | print(" marked broken", flush=True) 80 | did_one = True 81 | subprocess.check_call(f"git rm {file_name}", shell=True) 82 | subprocess.check_call( 83 | f"git commit -m 'Remove {file_name} after marking broken'", shell=True) 84 | subprocess.check_call("git show", shell=True) 85 | 86 | return did_one 87 | 88 | 89 | def mark_not_broken_file(file_name): 90 | did_one = False 91 | 92 | with open(file_name, "r") as f: 93 | pkgs = f.readlines() 94 | pkgs = [pkg.strip() for pkg in pkgs] 95 | for pkg in pkgs: 96 | # ignore blank lines or Python-style comments 97 | if pkg.startswith('#') or len(pkg) == 0: 98 | continue 99 | print(" package: %s" % pkg, flush=True) 100 | plat, name, ver, build = split_pkg(pkg) 101 | r = requests.delete( 102 | "https://api.anaconda.org/channels/conda-forge/broken", 103 | headers={'Authorization': 'token {}'.format(os.environ["BINSTAR_TOKEN"])}, 104 | json={ 105 | "basename": pkg, 106 | "package": name, 107 | "version": ver, 108 | } 109 | ) 110 | if r.status_code != 201: 111 | print(" could not mark not broken", flush=True) 112 | return did_one 113 | else: 114 | print(" marked not broken", flush=True) 115 | did_one = True 116 | subprocess.check_call(f"git rm {file_name}", shell=True) 117 | subprocess.check_call( 118 | f"git commit -m 'Remove {file_name} after marking not broken'", shell=True) 119 | subprocess.check_call("git show", shell=True) 120 | 121 | return did_one 122 | 123 | 124 | def mark_broken(): 125 | if "BINSTAR_TOKEN" not in os.environ: 126 | return 127 | 128 | did_any = False 129 | br_files = get_broken_files() 130 | print("found files: %s" % br_files, flush=True) 131 | for file_name in br_files: 132 | print("working on file %s" % file_name, flush=True) 133 | did_any = did_any or mark_broken_file(file_name) 134 | 135 | nbr_files = get_not_broken_files() 136 | print("found files: %s" % nbr_files, flush=True) 137 | for file_name in nbr_files: 138 | print("working on file %s" % file_name, flush=True) 139 | did_any = did_any or mark_not_broken_file(file_name) 140 | 141 | if did_any: 142 | with tempfile.TemporaryDirectory() as tmpdir: 143 | subprocess.check_call( 144 | "git clone https://github.com/conda-forge/" 145 | "conda-forge-repodata-patches-feedstock.git", 146 | cwd=tmpdir, 147 | shell=True, 148 | ) 149 | 150 | subprocess.check_call( 151 | "git remote set-url --push origin " 152 | "https://x-access-token:${GITHUB_TOKEN}@github.com/conda-forge/" 153 | "conda-forge-repodata-patches-feedstock.git", 154 | cwd=os.path.join(tmpdir, "conda-forge-repodata-patches-feedstock"), 155 | shell=True, 156 | ) 157 | 158 | all_files = br_files + nbr_files 159 | fstr = " ".join(f for f in all_files) 160 | subprocess.check_call( 161 | "git commit --allow-empty -am 'resync repo data " 162 | "for broken/notbroken packages in files %s'" % fstr, 163 | cwd=os.path.join(tmpdir, "conda-forge-repodata-patches-feedstock"), 164 | shell=True, 165 | ) 166 | 167 | subprocess.check_call( 168 | "git push", 169 | cwd=os.path.join(tmpdir, "conda-forge-repodata-patches-feedstock"), 170 | shell=True, 171 | ) 172 | 173 | 174 | if __name__ == "__main__": 175 | if len(sys.argv) != 2: 176 | raise RuntimeError("Need 1 and only 1 argument") 177 | if sys.argv[1] == 'check': 178 | check_packages() 179 | elif sys.argv[1] == 'mark': 180 | mark_broken() 181 | else: 182 | raise RuntimeError(f"Unrecognized argument {sys.argv[1]}") 183 | -------------------------------------------------------------------------------- /token_reset.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import glob 4 | import requests 5 | import subprocess 6 | import tempfile 7 | 8 | SMITHY_CONF = os.path.expanduser('~/.conda-smithy') 9 | 10 | 11 | def get_token_reset_files(): 12 | return ( 13 | [ 14 | f for f in glob.glob("token_reset/*") 15 | if f != "token_reset/example.txt" 16 | ] 17 | ) 18 | 19 | 20 | def feedstock_token_exists(organization, name): 21 | r = requests.get( 22 | "https://api.github.com/repos/%s/" 23 | "feedstock-tokens/contents/tokens/%s.json" % (organization, name), 24 | headers={"Authorization": "token %s" % os.environ["GITHUB_TOKEN"]}, 25 | ) 26 | if r.status_code != 200: 27 | return False 28 | else: 29 | return True 30 | 31 | 32 | def write_token(name, token): 33 | with open(os.path.join(SMITHY_CONF, name + '.token'), 'w') as fh: 34 | fh.write(token) 35 | 36 | 37 | def delete_feedstock_token(org, feedstock_name): 38 | with tempfile.TemporaryDirectory() as tmpdir: 39 | subprocess.check_call( 40 | "git clone " 41 | "https://x-access-token:${GITHUB_TOKEN}@github.com/conda-forge/" 42 | "feedstock-tokens.git", 43 | cwd=tmpdir, 44 | shell=True, 45 | ) 46 | 47 | subprocess.check_call( 48 | "git remote set-url --push origin " 49 | "https://x-access-token:${GITHUB_TOKEN}@github.com/conda-forge/" 50 | "feedstock-tokens.git", 51 | cwd=os.path.join(tmpdir, "feedstock-tokens"), 52 | shell=True, 53 | ) 54 | 55 | subprocess.check_call( 56 | "git rm tokens/%s.json" % feedstock_name, 57 | cwd=os.path.join(tmpdir, "feedstock-tokens"), 58 | shell=True, 59 | ) 60 | 61 | subprocess.check_call( 62 | "git commit --allow-empty -am " 63 | "'[ci skip] [skip ci] [cf admin skip] ***NO_CI*** removing " 64 | "token for %s'" % feedstock_name, 65 | cwd=os.path.join(tmpdir, "feedstock-tokens"), 66 | shell=True, 67 | ) 68 | 69 | subprocess.check_call( 70 | "git pull", 71 | cwd=os.path.join(tmpdir, "feedstock-tokens"), 72 | shell=True, 73 | ) 74 | 75 | subprocess.check_call( 76 | "git push", 77 | cwd=os.path.join(tmpdir, "feedstock-tokens"), 78 | shell=True, 79 | ) 80 | 81 | 82 | def reset_feedstock_token(name, skips=None): 83 | skips = skips or [] 84 | 85 | owner_info = ['--organization', 'conda-forge'] 86 | token_repo = ( 87 | 'https://x-access-token:${GITHUB_TOKEN}@github.com/' 88 | 'conda-forge/feedstock-tokens' 89 | ) 90 | 91 | with tempfile.TemporaryDirectory() as tmpdir: 92 | feedstock_dir = os.path.join(tmpdir, name + "-feedstock") 93 | os.makedirs(feedstock_dir) 94 | 95 | if feedstock_token_exists("conda-forge", name + "-feedstock"): 96 | delete_feedstock_token("conda-forge", name + "-feedstock") 97 | 98 | subprocess.check_call( 99 | ['conda', 'smithy', 'generate-feedstock-token', 100 | '--feedstock_directory', feedstock_dir] + owner_info) 101 | subprocess.check_call( 102 | [ 103 | 'conda', 'smithy', 'register-feedstock-token', 104 | '--without-circle', '--without-drone', 105 | ] 106 | + [ 107 | s for s in skips 108 | if s not in ["--without-circle", "--without-drone"] 109 | ] 110 | + [ 111 | '--feedstock_directory', feedstock_dir, 112 | ] 113 | + owner_info 114 | + ['--token_repo', token_repo] 115 | ) 116 | 117 | subprocess.check_call( 118 | [ 119 | 'conda', 'smithy', 'rotate-binstar-token', 120 | '--without-appveyor', '--without-azure', 121 | '--without-circle', '--without-drone', 122 | '--without-github-actions', 123 | ] 124 | + [ 125 | s for s in skips 126 | if s not in [ 127 | "--without-circle", 128 | "--without-drone", 129 | "--without-appveyor", 130 | "--without-azure", 131 | "--without-github-actions", 132 | ] 133 | ] 134 | + [ 135 | '--token_name', 'STAGING_BINSTAR_TOKEN' 136 | ], 137 | cwd=feedstock_dir) 138 | 139 | 140 | def reset_feedstock_tokens_in_file(token_reset_file): 141 | pkgs_to_do_again = [] 142 | skips = [] 143 | with open(token_reset_file, "r") as fp: 144 | for line in fp.readlines(): 145 | line = line.strip() 146 | if line.startswith("#") or len(line) == 0: 147 | if line.startswith("#") and "--without-" in line: 148 | skips.append(line[1:].strip()) 149 | continue 150 | 151 | try: 152 | reset_feedstock_token(line, skips=skips) 153 | except Exception as e: 154 | print( 155 | "failed to reset token for '%s': %s" % (line, repr(e)), 156 | flush=True, 157 | ) 158 | pkgs_to_do_again.append(line) 159 | 160 | if pkgs_to_do_again: 161 | with open(token_reset_file, "w") as fp: 162 | fp.write( 163 | "# token reset failed for these packages - " 164 | "trying again later\n" 165 | ) 166 | for pkg in pkgs_to_do_again: 167 | fp.write(pkg + "\n") 168 | subprocess.check_call(f"git add {token_reset_file}", shell=True) 169 | subprocess.check_call( 170 | f"git commit --allow-empty -m 'Keeping {token_reset_file} " 171 | "after failed token reset'", 172 | shell=True, 173 | ) 174 | else: 175 | subprocess.check_call(f"git rm {token_reset_file}", shell=True) 176 | subprocess.check_call( 177 | f"git commit -m 'Remove {token_reset_file} after token reset'", 178 | shell=True, 179 | ) 180 | 181 | subprocess.check_call("git show", shell=True) 182 | 183 | 184 | def check_for_feedstocks_in_file(token_reset_file): 185 | missing_feedstocks = [] 186 | with open(token_reset_file, "r") as fp: 187 | for line in fp.readlines(): 188 | line = line.strip() 189 | if line.startswith("#") or len(line) == 0: 190 | continue 191 | 192 | r = requests.get( 193 | "https://github.com/conda-forge/%s-feedstock" % line 194 | ) 195 | if r.status_code != 200: 196 | missing_feedstocks.append(line) 197 | return missing_feedstocks 198 | 199 | 200 | def main(): 201 | mode = sys.argv[1] 202 | 203 | if mode == "reset": 204 | if not os.path.exists(SMITHY_CONF): 205 | os.makedirs(SMITHY_CONF, exist_ok=True) 206 | 207 | for token_fname, token_name in [ 208 | ("circle", "CIRCLE_TOKEN"), 209 | ("azure", "AZURE_TOKEN"), 210 | ("drone", "DRONE_TOKEN"), 211 | ("travis", "TRAVIS_TOKEN"), 212 | ("github", "GITHUB_TOKEN"), 213 | ("anaconda", "STAGING_BINSTAR_TOKEN"), 214 | ]: 215 | if token_name in os.environ: 216 | write_token(token_fname, os.environ[token_name]) 217 | 218 | token_reset_files = get_token_reset_files() 219 | missing_feedstocks = [] 220 | for token_reset_file in token_reset_files: 221 | print("working on file %s" % token_reset_file, flush=True) 222 | if mode == "reset": 223 | reset_feedstock_tokens_in_file(token_reset_file) 224 | else: 225 | missing_feedstocks.extend( 226 | check_for_feedstocks_in_file(token_reset_file) 227 | ) 228 | 229 | if missing_feedstocks: 230 | raise RuntimeError( 231 | "feedstocks %s could not be found!" % missing_feedstocks 232 | ) 233 | 234 | 235 | if __name__ == "__main__": 236 | main() 237 | --------------------------------------------------------------------------------