Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add GHA action #7

Merged
merged 18 commits into from
May 16, 2024
15 changes: 15 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,18 @@ jobs:
- name: Build recipe (${{ env.PIXI_ENV_NAME }})
if: matrix.python-version == '310'
run: pixi run build

action:
runs-on: ubuntu-latest
permissions:
contents: write # to deploy to GH Pages automatically
steps:
- name: Checkout
uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5
with:
fetch-depth: 0
- uses: ./
with:
channel: conda-forge
keep-trees: python=3.9
gh-pages-branch: '' # disable on main
33 changes: 33 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,36 @@
# conda-subchannel

Create subsets of conda channels thanks to CEP-15 metadata

## conda plugin

```bash
$ conda install -n base conda-subchannel
$ conda subchannel --channel=conda-forge python=3.9
$ python -m http.serve --directory subchannel/
```

## Github Actions action

```yaml
name: Create conda subchannel

on:
push:
branches:
- main
pull_request:

jobs:
build-and-deploy:
runs-on: ubuntu-latest
permissions:
contents: write # to deploy to GH Pages automatically
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
steps:
- uses: jaimergp/conda-subchannel@main
with:
channel: conda-forge
keep-trees: python=3.9
```
124 changes: 124 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
name: 'conda subchannel'
description: 'Republish a subset of an existing conda channel'
inputs:
channel:
description: "Source conda channel"
required: true
repodata-fn:
description: "Source repodata file to process from channel"
required: false
default: "repodata.json"
subdirs:
description: "List of platforms to support, space separated. Defaults to linux-64. Noarch is always included"
required: false
default: "linux-64"
after:
description: "Timestamp as ts:<float> or date as YYYY-[MM[-DD[-HH[-MM[-SS]]]]]"
required: false
default: ""
before:
description: "Timestamp as ts:<float> or date as YYYY-[MM[-DD[-HH[-MM[-SS]]]]]"
required: false
default: ""
keep-trees:
description: "Keep packages matching these specs and their dependencies. Space separated"
required: false
default: ""
keep-specs:
description: "Keep packages matching these specs only. Space separated"
required: false
default: ""
remove-specs:
description: "Remove packages matching these specs. Space separated"
required: false
default: ""
gh-pages-branch:
description: "Name of the branch for the GH Pages deployment. Set to `''` to disable."
required: false
default: gh-pages
outputs:
output-directory:
description: "Path to the directory containing the subchannel data"
value: ${{ steps.validate.outputs.output-path }}
runs:
using: "composite"
steps:
- name: Check Runner OS
if: ${{ runner.os != 'Linux' }}
shell: bash
run: |
echo "::error title=⛔ error hint::Only Linux is supported"
exit 1
- name: Validate arguments
shell: bash
id: validate
run: |
if [[
"${{ inputs.after }}" == ""
&& "${{ inputs.before }}" == ""
&& "${{ inputs.keep-trees }}" == ""
&& "${{ inputs.keep-specs }}" == ""
&& "${{ inputs.remove-specs }}" == ""
]]; then
echo "::error title=⛔ error hint::At least one of `after`, `before`, `keep-trees`, `keep-specs` or `remove-specs` must be set"
exit 1
fi
mkdir -p "${{ runner.temp }}/subchannel"
echo "output-directory=${{ runner.temp }}/subchannel" >> $GITHUB_OUTPUT
- uses: prefix-dev/setup-pixi@632d17935141ec801697e2c359784b878adecbbe # v0.6.0
with:
environments: default
manifest-path: ${{ github.action_path }}/pyproject.toml
- name: Setup project
shell: bash
run: cd "${{ github.action_path }}" && pixi run --environment default dev
- name: Run subchannel
shell: pixi run --manifest-path "${{ github.action_path }}/pyproject.toml" --environment default bash -e {0}
run: |
args="--channel ${{ inputs.channel }}"
args+=" --output ${{ steps.validate.outputs.output-directory }}"
args+=" --repodata-fn ${{ inputs.repodata-fn }}"
for subdir in ${{ inputs.subdirs }}; do
args+=" --subdir $subdir"
done
if [[ "${{ inputs.after }}" != "" ]]; then
args+=" --after ${{ inputs.after }}"
fi
if [[ "${{ inputs.before }}" != "" ]]; then
args+=" --before ${{ inputs.before }}"
fi
if [[ "${{ inputs.keep-trees }}" != "" ]]; then
for spec in ${{ inputs.keep-trees }}; do
args+=" --keep-tree $spec"
done
fi
if [[ "${{ inputs.keep-specs }}" != "" ]]; then
for spec in ${{ inputs.keep-specs }}; do
args+=" --keep $spec"
done
fi
if [[ "${{ inputs.remove-specs }}" != "" ]]; then
for spec in ${{ inputs.remove-specs }}; do
args+=" --remove $spec"
done
fi
echo "Running: conda subchannel $args"
conda subchannel $args
- name: Decide deployment
id: decide
shell: bash
run: |
if [[ "${{ inputs.gh-pages-branch }}" != "" && "${{ github.ref }}" == "refs/heads/main" ]]; then
echo "deploy=true" >> $GITHUB_OUTPUT
else
echo "Will skip deployment to GH Pages."
echo "deploy=false" >> $GITHUB_OUTPUT
fi
- uses: peaceiris/actions-gh-pages@4f9cc6602d3f66b9c108549d475ec49e8ef4d45e # v4.0.0
if: steps.decide.outputs.deploy == 'true'
with:
github_token: ${{ github.token }}
publish_branch: ${{ inputs.gh-pages-branch }}
publish_dir: ${{ steps.validate.outputs.output-directory }}
user_name: 'github-actions[bot]'
user_email: 'github-actions[bot]@users.noreply.github.com'
17 changes: 12 additions & 5 deletions conda_subchannel/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,13 @@ def date_argument(date: str) -> float:


def configure_parser(parser: argparse.ArgumentParser):
parser.add_argument("-c", "--channel", required=True, dest="channel")
parser.add_argument(
"-c",
"--channel",
required=True,
dest="channel",
help="Source conda channel.",
)
parser.add_argument(
"--repodata-fn",
default=REPODATA_FN,
Expand Down Expand Up @@ -93,9 +99,10 @@ def execute(args: argparse.Namespace) -> int:
raise ArgumentError("Please provide at least one filter.")

with Spinner("Syncing source channel"):
subdir_datas = _fetch_channel(
args.channel, args.subdirs or context.subdirs, args.repodata_fn
)
subdirs = args.subdirs or context.subdirs
if "noarch" not in subdirs:
subdirs = *subdirs, "noarch"
subdir_datas = _fetch_channel(args.channel, subdirs, args.repodata_fn)
for sd in sorted(subdir_datas, key=lambda sd: sd.channel.name):
print(" -", sd.channel.name, sd.channel.subdir)

Expand All @@ -117,6 +124,6 @@ def execute(args: argparse.Namespace) -> int:

with Spinner(f"Writing output to {args.output}"):
repodatas = _dump_records(records, args.channel)
_write_to_disk(repodatas, args.output)
_write_to_disk(args.channel, repodatas, args.output)

return 0
55 changes: 55 additions & 0 deletions conda_subchannel/core.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
from __future__ import annotations

import bz2
import hashlib
import json
import logging
from datetime import datetime, timezone
from pathlib import Path
from typing import TYPE_CHECKING

Expand Down Expand Up @@ -162,7 +164,55 @@ def _dump_records(
return repodatas


def _checksum(path, algorithm, buffersize=65536):
hash_impl = getattr(hashlib, algorithm)()
with open(path, "rb") as f:
for block in iter(lambda: f.read(buffersize), b""):
hash_impl.update(block)
return hash_impl.hexdigest()


def _write_channel_index_md(source_channel: Channel, channel_path: Path):
channel_path = Path(channel_path)
lines = [
f"# {channel_path.name}",
"",
f"Derived from [{source_channel.name}]({source_channel.base_url})",
"",
""
]
for subdir in channel_path.glob("*"):
if subdir.is_file():
continue
lines[-1] += f"[{subdir.name}]({subdir.name}) "
(channel_path / "index.md").write_text("\n".join(lines))


def _write_subdir_index_md(subdir_path: Path):
subdir_path = Path(subdir_path)
lines = [
f"# {'/'.join(subdir_path.parts[-2:])}",
"| Filename | Size (B) | Last modified | SHA256 | MD5 |",
"|----------|----------|---------------|--------|-----|",
]
for path in sorted(subdir_path.glob("*")):
if path.name == "index.md":
continue
stat = path.stat()
size = stat.st_size
lastmod = datetime.fromtimestamp(stat.st_mtime)
sha256 = _checksum(path, "sha256")
md5 = _checksum(path, "md5")
lines.append(
f"| [{path.name}]({path.name}) | {size} | {lastmod} | `{sha256}` | `{md5}` |"
)
lines.append("")
lines.append(f"> Last modified on {datetime.now(tz=timezone.utc)}")
(subdir_path / "index.md").write_text("\n".join(lines))


def _write_to_disk(
source_channel: Channel | str,
repodatas: dict[str, dict[str, Any]],
path: os.PathLike | str,
outputs: Iterable[str] = ("bz2", "zstd"),
Expand All @@ -185,8 +235,13 @@ def _write_to_disk(
level=ZSTD_COMPRESS_LEVEL, threads=ZSTD_COMPRESS_THREADS
).compress(json_contents.encode("utf-8"))
fo.write(repodata_zst_content)
_write_subdir_index_md(path / subdir)

# noarch must always be present
noarch_repodata = path / "noarch" / "repodata.json"
if not noarch_repodata.is_file():
noarch_repodata.parent.mkdir(parents=True, exist_ok=True)
noarch_repodata.write_text("{}")
_write_subdir_index_md(path / "noarch")

_write_channel_index_md(Channel(source_channel), path)
Loading