Skip to content
This repository was archived by the owner on Dec 29, 2024. It is now read-only.

Commit 9bb7059

Browse files
FlacyAndrew Krylov
andauthored
perf: rework GHA workflow (#17)
* feat: add `setup.py` file * fix: optimize setup actions * fix: optimize workflow * perf: add 'CD' workflow * test * fix: exe file name * fix: test version * fix: env variable * fix: build job * fix: remove env from matrix * fix: path to exe file * fix: python build * fix: debug * fix: debug * fix: debug * fix: debug * fix: debug * fix: artifact overwriting * fix: file glob * fix: debug * fix: debug * fix: debug * fix: debug * fix: file resolving * fix: file resolving * fix: allow overwriting * fix: file resolving * perf: micro optimization * fix: test bump version * perf: optimizing and documenting workflow * chore: remove on "perf/gha" trigger * perf: add test on different versions * fix: optimize jobs * fix: remove legacy go versions * fix: os name * fix: documenting workflow * chore: bump version --------- Co-authored-by: Andrew Krylov <any@unte.dev>
1 parent b3da097 commit 9bb7059

5 files changed

Lines changed: 312 additions & 16 deletions

File tree

.github/workflows/cd.yml

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
name: "CD"
2+
3+
on:
4+
push:
5+
branches:
6+
- "main"
7+
8+
env:
9+
# Use latest stable version.
10+
GO_VERSION: "stable"
11+
PY_VERSION: "3.12"
12+
PY_DIST_DIRECTORY: "whl"
13+
14+
jobs:
15+
update-version:
16+
name: "Create git tag"
17+
runs-on: ubuntu-latest
18+
19+
outputs:
20+
# Semantic version.
21+
from-config: ${{ steps.extract.outputs.version }}
22+
# "v" + Semantic version.
23+
tag: ${{ steps.tag.outputs.new }}
24+
25+
steps:
26+
- uses: actions/checkout@v4
27+
- name: "Extract version from config file"
28+
id: extract
29+
# Extract the string value of the constant "Version" from the .go file.
30+
run: |
31+
v=$(grep -oP 'Version\s*=\s*"\K[^"]+' fext/config/config.go)
32+
echo "version=$v" >> "$GITHUB_OUTPUT"
33+
- name: "Setup git config"
34+
# Inside the container, git config is empty,
35+
# so we fill it similarly to the action initiator.
36+
run: |
37+
latest_commit=$(git log -1 --pretty=format:"%an|%ae")
38+
IFS='|' read -r author_name author_email <<< "$latest_commit"
39+
git config --global user.email "$author_email"
40+
git config --global user.name "$author_name"
41+
- name: "Create tag"
42+
id: tag
43+
# Add a new tag to Git, pulled from the config.
44+
# The authorship goes to the initiator of the latest commit.
45+
run: |
46+
tag="v${{ steps.extract.outputs.version }}"
47+
current_date=$(date +"%d %B %Y")
48+
git tag -a "$tag" -m "Release $tag of $current_date"
49+
git push origin "$tag"
50+
echo "new=$tag" >> "$GITHUB_OUTPUT"
51+
52+
package:
53+
name: "Build and package"
54+
runs-on: ${{ matrix.os }}
55+
needs:
56+
- update-version
57+
58+
strategy:
59+
matrix:
60+
include:
61+
- os: ubuntu-latest
62+
exe_file: "fext"
63+
platform: "linux"
64+
platform_tag: "manylinux_2_35_x86_64"
65+
- os: windows-latest
66+
exe_file: "fext.exe"
67+
platform: "windows"
68+
platform_tag: "win_amd64"
69+
70+
steps:
71+
# Prepare environment.
72+
- uses: actions/checkout@v4
73+
- uses: actions/setup-go@v5
74+
with:
75+
go-version: ${{ env.GO_VERSION }}
76+
- uses: actions/setup-python@v5
77+
with:
78+
python-version: ${{ env.PY_VERSION }}
79+
check-latest: true
80+
# Compile into binary file.
81+
- name: "Build application"
82+
run: cd fext && go build -o "dist/${{ matrix.exe_file }}"
83+
# Package into wheel file.
84+
- name: "Packaging application"
85+
run: make build
86+
env:
87+
FEXT_PLATFORM_TAG: ${{ matrix.platform_tag }}
88+
FEXT_VERSION: ${{ needs.update-version.outputs.from-config }}
89+
FEXT_EXE_FILE: "fext/dist/${{ matrix.exe_file }}"
90+
# This step blocks the execution of the package publication job,
91+
# ensuring that we publish packages for ALL specified platforms.
92+
# If an error occurs within this job, the publication won't be executed.
93+
- name: "Transfer package to the next job"
94+
uses: actions/upload-artifact@v4
95+
with:
96+
# Artifact doesn't support adding files to an existing storage,
97+
# so it creates a separate one for each platform.
98+
name: "pkg-${{ matrix.platform }}"
99+
# the Make script saves the packaged application into this directory
100+
# located within a $GITHUB_WORKSPACE.
101+
path: ${{ env.PY_DIST_DIRECTORY }}
102+
103+
publish:
104+
name: "Upload to the GitHub release"
105+
runs-on: ubuntu-latest
106+
107+
needs:
108+
- update-version
109+
- package
110+
111+
steps:
112+
# Duplicate is necessary to ensure the correctness of execution steps.
113+
- name: "Download linux package"
114+
uses: actions/download-artifact@v4
115+
with:
116+
name: "pkg-linux"
117+
path: ${{ env.PY_DIST_DIRECTORY }}
118+
- name: "Download windows package"
119+
uses: actions/download-artifact@v4
120+
with:
121+
name: "pkg-windows"
122+
path: ${{ env.PY_DIST_DIRECTORY }}
123+
# Upload all packaged files to the corresponding release on GitHub.
124+
- name: "Upload packages into release"
125+
uses: svenstaro/upload-release-action@v2
126+
with:
127+
file: "${{ env.PY_DIST_DIRECTORY }}/*"
128+
file_glob: true
129+
tag: ${{ needs.update-version.outputs.tag }}

.github/workflows/ci.yml

Lines changed: 48 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,60 @@
1-
name: CI
1+
name: "CI"
22

3+
# There's no need to run the workflow on every commit,
4+
# so it only runs on final changes.
35
on:
46
push:
5-
branches: [ "main" ]
7+
branches:
8+
- "main"
69
pull_request:
7-
branches: [ "main" ]
10+
branches:
11+
- "main"
812

9-
jobs:
13+
env:
14+
# Use latest stable version.
15+
GO_VERSION: "stable"
1016

11-
build:
17+
jobs:
18+
coverage:
19+
name: "Check coverage"
1220
runs-on: ubuntu-latest
13-
steps:
14-
- uses: actions/checkout@v3
1521

16-
- name: Set up Go
17-
uses: actions/setup-go@v4
22+
steps:
23+
- uses: actions/checkout@v4
24+
- uses: actions/setup-go@v5
1825
with:
19-
go-version: '1.20'
20-
21-
- name: Test
26+
go-version: ${{ env.GO_VERSION }}
27+
- name: "Generate reports"
2228
run: cd fext && go test -race ./... -coverprofile=coverage.out -covermode=atomic
23-
24-
- name: Upload coverage reports to Codecov
25-
uses: codecov/codecov-action@v3
29+
- name: "Upload reports to Codecov"
30+
uses: codecov/codecov-action@v4
2631
env:
2732
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
33+
34+
test:
35+
# Test how compilation and tests pass on different platforms and versions of Go.
36+
name: "Test on version"
37+
# The important thing is not the version of the platform it will run on,
38+
# but rather the platform itself.
39+
runs-on: ${{ matrix.os }}-latest
40+
41+
strategy:
42+
matrix:
43+
os:
44+
- ubuntu
45+
- windows
46+
# macOS promises to someday appear here...
47+
go-version:
48+
- "1.22"
49+
- "1.21"
50+
- "1.20"
51+
- "1.19"
52+
- "1.18"
53+
54+
steps:
55+
- uses: actions/checkout@v4
56+
- uses: actions/setup-go@v5
57+
with:
58+
go-version: ${{ matrix.go-version }}
59+
- name: "Test application"
60+
run: cd fext && go test -race ./...

Makefile

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
.SILENT:
2+
3+
install-builder:
4+
pip install setuptools wheel > /dev/null
5+
6+
build: install-builder
7+
python setup.py bdist_wheel --plat-name=${FEXT_PLATFORM_TAG} --dist-dir=${PY_DIST_DIRECTORY}

fext/config/config.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import (
1010
)
1111

1212
const (
13-
Version = "0.4.1"
13+
Version = "0.4.2.dev0"
1414
DefaultChmod = 0755
1515

1616
MarkerPythonImpl = "CPython" // platform_python_implementation (platform.python_implementation())

setup.py

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
"""
2+
Sometimes unconventional solutions are necessary to tackle unconventional situations.
3+
We inherit from the wheel package to directly package the application into it,
4+
avoiding the need to invent custom formats.
5+
This allows for efficient utilization of a unified parser supported by Python itself.
6+
7+
Originally hosted on PyPI, but unfortunately removed for unknown reasons.
8+
Thus, the name "fext-cli" is taken, ensuring no conflicts.
9+
Currently, GitHub Releases are used to store the compiled application in a wheel package.
10+
11+
For initial installation, the script from "github.com/fextpkg/get" is used.
12+
Self-update functionality is expected soon.
13+
"""
14+
15+
import os
16+
from os.path import basename
17+
18+
from setuptools import setup
19+
from setuptools.command.install import install
20+
21+
22+
def retrieve_env_variable(key: str) -> str:
23+
"""
24+
Retrieves value from the environment variable.
25+
26+
:raise RuntimeError: If it's not set.
27+
"""
28+
if not (v := os.environ.get(key)):
29+
raise RuntimeError(f"Environment variable {key} is not specified")
30+
31+
return v
32+
33+
34+
class Env:
35+
# Environment variables names.
36+
# Package version.
37+
VERSION: str = retrieve_env_variable("FEXT_VERSION")
38+
# Path to executable file.
39+
EXE_FILE: str = retrieve_env_variable("FEXT_EXE_FILE")
40+
41+
42+
class OverrideCommand(install):
43+
"""
44+
Built-in setuptools commands don't support straightforward addition of binary files.
45+
More precisely, they **don't allow** adding them to scripts.
46+
We understand Python's stance on this matter,
47+
but we want to **avoid impacting** Python in any way because its execution consumes many resources.
48+
49+
To address this issue, we modified this command to create an empty-package
50+
containing only metadata and a scripts directory with the binary file itself.
51+
52+
Unfortunately, no builder can be configured as flexibly as setuptools itself.
53+
Consequently, none can support such commands without workarounds.
54+
It's not the best solution, but at least it's easy to maintain.
55+
56+
Yes, direct invocation of ``setup.py`` is deprecated, but there's currently **no alternative**.
57+
"""
58+
59+
# Working directory.
60+
source_dir: str = os.path.dirname(os.path.realpath(__file__))
61+
62+
def run(self):
63+
"""
64+
The magical installation command that creates a bit of mess inside the package.
65+
"""
66+
# As a precaution, run the original command just in case.
67+
super().run()
68+
69+
# If the directory hasn't been created yet, create it.
70+
if not os.path.isdir(self.install_scripts):
71+
os.makedirs(self.install_scripts)
72+
73+
# Specify both the external and internal paths to the executable file.
74+
source: str = os.path.join(self.source_dir, Env.EXE_FILE)
75+
target: str = os.path.join(self.install_scripts, basename(source))
76+
77+
# If it happens that it already exists, remove it to avoid errors.
78+
if os.path.isfile(target):
79+
os.remove(target)
80+
81+
# And perform a dirty trick.
82+
self.copy_file(source, target)
83+
84+
85+
class Builder:
86+
def __init__(self) -> None:
87+
# Prepare data
88+
self.description, self.description_type = self.get_description()
89+
90+
def setup(self) -> None:
91+
"""
92+
Builds the package using ``setuptools``.
93+
"""
94+
setup(
95+
# General information.
96+
name="fext-cli",
97+
version=Env.VERSION,
98+
description="Fast & Modern package manager",
99+
long_description=self.description,
100+
long_description_content_type=self.description_type,
101+
license="MIT",
102+
author="Andrew Krylov",
103+
author_email="any@lunte.dev",
104+
url="https://github.com/fextpkg/cli",
105+
keywords=["fast", "modern", "package", "manager"],
106+
# Ignore errors related to empty package
107+
# while simultaneously optimizing the package size.
108+
packages=[],
109+
# Leverage the ability to store external files.
110+
include_package_data=True,
111+
# The magic lies in overriding the installation command.
112+
cmdclass={"install": OverrideCommand},
113+
# Don't generate in ".egg" format.
114+
zip_safe=False,
115+
)
116+
117+
@staticmethod
118+
def get_description() -> tuple[str, str]:
119+
"""
120+
Retrieves the text and type of README file.
121+
"""
122+
with open("README.md", "r", encoding="utf-8") as f:
123+
return f.read(), "text/markdown"
124+
125+
126+
if __name__ == "__main__":
127+
Builder().setup()

0 commit comments

Comments
 (0)