diff --git a/.github/workflows/autotag.yaml b/.github/workflows/autotag.yaml deleted file mode 100644 index 3d7fd71..0000000 --- a/.github/workflows/autotag.yaml +++ /dev/null @@ -1,18 +0,0 @@ -name: "Auto Tag" -on: - issues: - types: [opened, edited] - -permissions: - issues: write - contents: read - -jobs: - triage: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v1 - - uses: damccorm/tag-ur-it@master - with: - repo-token: "${{ secrets.GITHUB_TOKEN }}" - configuration-path: "config/issue-rules.yml" \ No newline at end of file diff --git a/.github/workflows/autotest.yml b/.github/workflows/autotest.yml deleted file mode 100644 index 03b2661..0000000 --- a/.github/workflows/autotest.yml +++ /dev/null @@ -1,22 +0,0 @@ -name: "Continuous Integration" - -on: push - -jobs: - test: - runs-on: ubuntu-24.04 - steps: - - name: Checkout Code - uses: actions/checkout@v4 - - - name: Setup Python - uses: actions/setup-python@v5 - with: - python-version: '3.12.4' - - - name: Install Dependencies - run: - pip install -r requirements.txt - - - name: Run Tests - run: pytest \ No newline at end of file diff --git a/.github/workflows/triage_issues.yml b/.github/workflows/triage_issues.yml deleted file mode 100644 index f47dcc7..0000000 --- a/.github/workflows/triage_issues.yml +++ /dev/null @@ -1,46 +0,0 @@ -name: "Label Issues for Triage" -on: - issues: - types: - - reopened - - opened -jobs: - label_issues: - runs-on: ubuntu-latest - permissions: - issues: write - env: - LABELS_JSON: | - [ - {"name": "triage", "color": "ededed", "description": "Triage needed"}, - ] - steps: - - uses: actions/github-script@v7 - with: - script: | - const labels = JSON.parse(process.env.LABELS_JSON); - for (const label of labels) { - try { - await github.rest.issues.createLabel({ - owner: context.repo.owner, - repo: context.repo.repo, - name: label.name, - description: label.description || '', - color: label.color - }); - } catch (error) { - // Check if the error is because the label already exists - if (error.status === 422) { - console.log(`Label '${label.name}' already exists. Skipping.`); - } else { - // Log other errors - console.error(`Error creating label '${label.name}': ${error}`); - } - } - } - - run: gh issue edit "$NUMBER" --add-label "$LABELS" - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - GH_REPO: ${{ github.repository }} - NUMBER: ${{ github.event.issue.number }} - LABELS: question diff --git a/.vscode/settings.json b/.vscode/settings.json index 8fc7789..37e28eb 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -12,5 +12,35 @@ "coverage-gutters.showLineCoverage": true, "python.testing.pytestArgs": [ "tests" - ] + ], + "files.exclude": { + "**/.git": true, + "**/.svn": true, + "**/.hg": true, + "**/CVS": true, + "**/.DS_Store": true, + "**/Thumbs.db": true, + "**/*.pyc": { + "when": "$(basename).py" + }, + "**/__pycache__": true, + "**/app.egg-info": true, + "**/env": true, + "**/.env.dist": true, + "**/*.log": true, + "**/.0": true + }, + "workbench.colorCustomizations": { + "tab.activeBorder": "#ff0000", + "tab.unfocusedActiveBorder": "#000000", + "tab.activeBackground": "#045980" + }, + "workbench.editor.wrapTabs": true, + "debug.toolBarLocation": "docked", + "python.formatting.provider": "autopep8", + "editor.formatOnSave": true, + "[python]": { + "editor.defaultFormatter": "ms-python.autopep8" + }, + "python.REPL.enableREPLSmartSend": false } \ No newline at end of file diff --git a/README.md b/README.md index f283c09..76e609e 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,12 @@ # GitHub Actions Experiments -This repository illustrates how GitHub Actions can be used to automate software development processes. GitHub Actions are scripts that run on a containerized platform hosted on GitHub. A GitHub action is defined by creating a `.yml` file in the `.github/workflows` directory of a repository (as done here). Such an action needs to be follow a specific format as described in the [GitHub documentation](https://docs.github.com/en/actions/about-github-actions/understanding-github-actions). An example is provided in the `triage_issues.yml` action file: +This repository illustrates how GitHub Actions can be used to automate software development processes. + +## Introduction + +### What are GitHub Actions? + +GitHub Actions are scripts that run on a containerized platform hosted on GitHub. A GitHub action is defined by creating a `.yml` file in the `.github/workflows` directory of a repository (as done here). Such an action needs to be follow a specific format as described in the [GitHub documentation](https://docs.github.com/en/actions/about-github-actions/understanding-github-actions). An example is provided in the `triage_issues.yml` action file: ```yaml name: "Label Issues for Triage" @@ -34,94 +40,280 @@ If you are familiar with **containerized applications**, you will notice familia Now that you understand the basic structure of a GitHub Action, we will see on in, well, action. -## Exercise 1 - Issue Action +### π Galactic Pizza Delivery Time Estimator + +In this exercise, you will work with a small application called the **Galactic Pizza Delivery Time Estimator**. The program calculates how long it takes to deliver a pizza to different planets using several factors: the destination distance, the delivery mode (normal, turbo, or hyperjump), a surge-load multiplier, and a weather-delay component. Some planets involve long-distance fatigue penalties, while incorrect inputs or unexpected weather values trigger errors. + +The application can be found in in the `spacedelivery` folder. There are two versions: + +- `python`: the Python version is a single module (`delivery.py`) and just one function. That module will be used for illustrating **building**, **testing**, and **releasing** an applications using GitHub Actions. +- `web` : the web version is a static web application of essentially the same functionality as the Python version. It also implments a user interface. This version is used to illustrate how to **deploy** a static web application to GitHub Pages via GitHub Actions. + +The application itself is secondary and only used to illustrate CI/CD concepts. + +### Directory structure + +The directory structure is as follows: + +```bash +actions/ +βββ .github/ # GitHub-specific configuration +β βββ workflows/ # This is where GitHub will look for `.yml` Action files. +βββ deployment_scripts/ # Python scripts that will be called by the *release* Action +βββ spacedelivery/ # Main application package +β βββ delivery.py # Python module for delivery calculations +β βββ web/ # Web version of the application to be deployed by the *deploy* Action +βββ tests/ # Tests that will be executed by the *test* Action +βββ __version__.py # Version information to be updated by the *release* Action +βββ requirements.txt # Python dependencies that will be installed during the *build* Action +``` + +### Getting Started + +Fork the repository and clone it to your local machine. Then create a new branch called `development`: + +```bash +git checkout -b development +``` + +All exercises will be done on the `development` branch that we will merge into the `main` branch occasionally. + +## Exercises -In this exercise, we will run our first GitHub Action It is already defined so you don't have to do too much to see it execute and the result of its execution. +This lab gradually builds up a CI/CD pipeline using GitHub Actions. As you know, Continuous Integration (CI) and Continuous Deployment (CD) are practices that help teams deliver software quickly and reliably. CI focuses on automatically **building** and **testing** code every time changes are made, ensuring that bugs are caught early and that new work integrates smoothly. CD extends this idea by automatically **releasing** and **deploying** software after it passes tests. GitHub Actions provides an easy way to implement CI/CD directly within a GitHub repository. By defining simple workflow files, you can automate tasks such as running tests, checking code quality, updating version numbers, or deploying applicationsβall triggered whenever code is pushed or a pull request is opened. -Let's assume we have developed an application that is open source and is wildly popular with developers. As developers use your application, they also find bugs and ways to improve the application. When they do, they usually submit a GitHub issue. We need a way of making sure that we triage all the new issues appropriately. In this exercise, we will work with a GitHub Action that automatically tags all new issues that are submitted with the label `triage`. +The diagram below shows the steps of the CI/CD pipeline that we will build in this lab: -To see the it in action, simply create a new issue with any title. Stay on the page and wait until the label `triage` magically appears. When it does, you know the GitHub Action did it's job. + -You can also view every run of a GitHub Action by clicking on `Actions` in the repository toolbar. The green checkmark indicates that the run was successful. Clicking on the job name, tells you more about that particular run. On the next page, click on the box that indicates the job that was executed as part of that action. Now you see all the details of the run. +We will implement GitHub Actions that are automatically triggered by events in GitHub, namely: +- `push` event: runs tests whenever a new commit is pushed to the repository +- `pull_request` event: runs tests whenever a new pull request is opened against the repository +- `merge` event: runs tests whenever the PR is merged into the `main` branch. -## Exercise 2 - Smarter Issue Labeling +These events will trigger GitHub Actions to build, test (`test.yml`), release (`release.yml`), and deploy (`deploy.yml`) the application. You will create this Actions as you follow the exercise instructions. -We have already seen an example of labeling issues automatically for triage. However, we might want GitHub Actions to perform smarter tagging by recognizing what type of issue the user submitted. We can implement it from scratch or we can use an [existing library](https://github.com/damccorm/tag-ur-it) to do most of the work for us. The repository description for [`tag-ur-it`](https://github.com/damccorm/tag-ur-it) describes how to set up and configure custom labeling. +## Exercise 1: **Testing** -Create a new file `.github/workflows/autotag.yaml` and copy in the following content: +One of the tasks that goes along with making changes to your code is testing. We have learned how to create test cases and even implemented unit tests that can be executed automatically. As part of a CI (Continuous Integration) pipeline, we can run tests automatically whenever a new commit is pushed to the repository. In this exercise, we will integrate that part of the CI pipeline using GitHub Actions. + +In the `.github/workflows` directory, create a new file names `test.yml` and copy the following contents: ```yaml -name: "Auto Tag" -on: - issues: - types: [opened, edited] +name: Build and test -permissions: - issues: write - contents: read +on: + push: + branches: ["development"] # Triggered when a commit is pushed to the development branch jobs: - triage: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v1 - - uses: damccorm/tag-ur-it@master - with: - repo-token: "${{ secrets.GITHUB_TOKEN }}" - configuration-path: "issue-rules.yml" + build: + runs-on: ubuntu-latest # Run this code on a Linux Virtual Machine (VM) + + steps: # Steps that are executed for this Action + + # Checks out the code into the VM + - name: Checkout repository + uses: actions/checkout@v4 + + # Installs Python + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.11" + + # Installs the dependencies + - name: Install Dependencies + run: pip install -r requirements.txt + + # Discovers and runs the tests + - name: Run tests + run: pytest -q ``` -The GitHub Action specified above is very simple: it executed when an issue is opened or re-opened. It then checks out your repository code and runs the [`tag-ur-it`](https://github.com/damccorm/tag-ur-it) job. It can do more than just label issues. The behavior is configured in `config/issue-rules.yml`. Note that the file specifies that if the term `enpm611` appears in the issue text, the issue is assigned to the GitHub user `enpm611`: +Save the file and push the changes to the repository: -```yaml -rules: -... -- contains: enpm611 - assign: ['enpm611'] +```bash +git add --all +git commit -m "Added test action" +git push ``` -After you created that file, push your changes to the repository. Then, work on the following tasks: +Now, go to your GitHub repository page in your browser and click on the Actions tab. You should see the test action running. Click on the action to reveal details about each step of the action (as defined in the `steps` section of the workflow file). + +Is your Action failing? That's ok. That's what we expect. Let's fix it. Check the details of the failed test output. It should tell you what test was failing and where in the code that failure occurred. Go to the respective test in `tests/test_delivery.py` and update the test case to match the value that is returned by the function. -* **Task A**: create a new rule that assigns the label `question` if the issue contains the term `maybe`. +Once you update the test case, push your changes to the repository with the same commands as above. Again, navigate to the Actions tab in GitHub to see the output of the test run. -* **Task B**: add a rule that assigns the issue to your user if the issue contains the term `urgent`. +This exercise illustrates one of the most fundamental parts of a CI/DC pipeline: automated quality assurance by executing test cases. That provides developers early warning signs that something is broken and should be fixed immediately. +## Exercise 2: **Release** -## Exercise 3 - Continuous Integration +Imagine you have completed making changes to the *Galactic Pizza Delivery Time Estimator* and are ready to push a new release out to your users. There are a variety of steps that could be entailed in a release process. For this exercise, we will have our GitHub Action automatically update the version of our application and create release notes. -Continuous Integration (CI) relies heavily on automating processes so that we an focus on development and let tools take care of giving us feedback when something goes wrong. Hence, a core part of CI is to automated the running of tests whenever someone pushes code to the repository. The goal is to get immediate feedback telling us that whether what we commited is of acceptable quality. +βΉοΈ [**Semantic versioning**](https://semver.org) is a standardized way of assigning version numbers so that users can understand the scope and impact of changes in a release. A semantic version has the form `MAJOR.MINOR.PATCH` (e.g., `1.8.16`). The `MAJOR` number increases when changes break backward compatibility, `MINOR` increases when new features are added without breaking existing behavior, and `PATCH` increases for backwards-compatible bug fixes. -Now, you will create a new GitHub Action. First, in the folder `.github/workflows`, create a file named `autotest.yml`. Then, copy and paste the following YAML specification into the file and save it: +Keep up with version can be tedious. So we will let GitHub Action handle the versioning for us. Create a new file in `.github/workflows` and copy the content below: ```yaml -name: "Continuous Integration" +name: Update version and add release notes to README -on: push +on: + pull_request: + types: [opened] # Only executed when a new PR is created +permissions: # Required for the Action to change our version and README files + contents: write + pull-requests: write + jobs: - test: - runs-on: ubuntu-24.04 + + update_readme: + + runs-on: ubuntu-latest + steps: - - name: Checkout Code + + - name: Checkout repository uses: actions/checkout@v4 + with: + ref: ${{ github.head_ref }} - - name: Setup Python + - name: Set up Python uses: actions/setup-python@v5 with: - python-version: '3.12.4' - - - name: Install Dependencies - run: - pip install -r requirements.txt + python-version: "3.x" - - name: Run Tests - run: pytest + # Calls a Python script to increase the version number in `__version__.py` + - name: Increase version number + env: + PR_TITLE: ${{ github.event.pull_request.title }} + run: | + python deployment_scripts/update_version_number.py + + # Calls a Python script to update the release notes in `README.md` + - name: Update release notes + env: + PR_TITLE: ${{ github.event.pull_request.title }} + PR_BODY: ${{ github.event.pull_request.body }} + run: | + python deployment_scripts/update_release_notes.py + + # Commits the changes the script made to `__version__.py` and `README.md` + - name: Commit changes + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git add __version__.py + git add README.md + git commit -m "Add release notes for PR #${{ github.event.pull_request.number }}" || echo "No changes to commit" + git push +``` + +The Action itself doesn't do a lot. It just calls Python scripts that reside in our repo (`deployment_scripts`): + +- `update_version_number.py`: reads the version number from `__version__.py` and increments it. +- `update_release_notes.py`: reads the PR title and body and adds them to the `README.md` file. + +These Python scripts use the information you enter in your PR title and description. To increase the version number, it checks if the PR title contains `major` or `minor`. If so, it will increase the respective part of the semantic version number. The description of the PR will be used as the release notes that are inserted into the README file. + +### Tasks A + +Once you created the `release.yml` Action file, push it to the repository as described above. Now, go to the `Pull request` tab of your GitHub repository. Create a new PR with the following info: + +- Title: `Emergency fix for cheese crust pizza` +- Body: + ``` + - Patching a bug found when calculating cheese crust pizza delivery cost. + - Slight UI improvements. + ``` + +Create the PR and go to the `Actions` tab. You should see the release Action running or already be completed. After the Action is completed, check the `Files changed` section in the PR for the changes made to `__version__.py` and `README.md`. + +β Can you see what part of the version number was updated (e.g., major, minor, patch)? Do you know why? + +We don't really want to merge this PR. Got ahead and click on `Close pull request` at the very bottom of the `Conversation` tab of the PR. + +### Tasks B + +Now, let's see if we can make our Action update the `Minor` part of the semantic version number. Create a new PR with the following details: + +- Title: `Minor update for delivery to Mars` +- Body: + + ```bash + - Can now calculate delivery to Mars! + - Minor bug fixes + ``` + +Create the PR and check again on the Action execution and then the changes it made. Did it work to increase the `minor` part of the version number? + +Let's keep this PR open since we'll use it in the next exercise. + +This exercise illustrated how we can use GitHub Actions to automate the release process, letting developers focus on being productive and leaving tedious task up to the CI/CD pipeline. + +## Exercise 3: Deploy + +To make our application available to users, we need to deploy it to some server. For this exercise, we will use GitHub Pages to host our application. GitHub pages. GitHub Pages is a free hosting service provided by GitHub that allows you to publish static websites directly from a GitHub repository. + +Create a file in `.github/wokflows` and name it `deploy.yml`. Then copy the contents below: + +```yml +name: Deploy static content to Pages + +on: + push: + branches: [ main ] # Runs when PR is merged to main branch or code is pushed directly to main + + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages +permissions: + contents: read + pages: write + id-token: write + +# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. +# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. +concurrency: + group: "pages" + cancel-in-progress: false + +jobs: + + deploy: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + steps: + + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Pages + uses: actions/configure-pages@v5 + + - name: Upload artifact + uses: actions/upload-pages-artifact@v3 + with: + path: "./spacedelivery/web" # directory containing the web app files + + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 ``` -This action is executed whenever someone does a `push`. It will perform several `steps` as part of the `test` job. First, it checks out the source code from your repository. We are using a pre-defined [checkout action](https://github.com/actions/checkout) for this. You can see that we can reuse actions also! Then, we set up a Python environment so that we can run our code using the [setup-python action](https://github.com/actions/setup-python). As you know, we can't execute our Python code if we don't first install our dependencies. So next, we are doing exactly that by running the `pip install` command. Finally, we can run our tests by running `pytest`. This will run all the test defined in this repository and let us know if the tests pass. +Now, push the changes to the `development` branch. Go to the PR that should still be open. If not, you can just create a new PR merging the `development` branch into the `main` branch. Next, click on `Merge pull request`. That event will trigger the deployment script above and deploy the app to gitHub pages. Go to the page: + +https://enpm611.github.io/github-actions + +but replace the user name to matched your forked repository. You should see the app running. + +This exercise illustated the last step of the CI/CD pipeline, which is delivering the application to its final destination from where users will be able to access and interact with it. You have now implemented your own CI/CD pipeline. You can find many more actions in the (GitHub Marketplace)[https://github.com/marketplace?type=actions]. -After you created the file and copied the action above, push the change to the repository. Next, work on the following tasks: -* **Task C**: Add a test case to either test file and push your changes to your repository. Check the run of the action to see what status is finishes with. +# Release notes -* **Task D**: You will notice that the action shows a red x after it has completed its run. Investigate why that action failed. Resolve the issue and push to the repository to trigger the action again. +This section should be populated by the *release* Action. diff --git a/__version__.py b/__version__.py new file mode 100644 index 0000000..beadd94 --- /dev/null +++ b/__version__.py @@ -0,0 +1,7 @@ +# coding: utf-8 + +__title__ = 'enpm611-ghactions' +__version__ = '1.4.0' +__author__ = 'ENPM611' +__url__ = 'https://github.com/enpm611/github-actions' +__description__ = ("Exercise to use GitHub Actions for CI/CD task.") diff --git a/app/string_utils.py b/app/string_utils.py deleted file mode 100644 index 3a57a95..0000000 --- a/app/string_utils.py +++ /dev/null @@ -1,25 +0,0 @@ -""" -Utility to perform advanced string comparisons. -""" - -from difflib import SequenceMatcher -import textdistance - - -def calculate_match_degree(txt1:str,txt2:str) -> float: - """ - Given two strings, finds the longest common substring. - Returns the degree of the match based on that longest - substring. - """ - match = SequenceMatcher(None, txt1, txt2).find_longest_match() - return match.size/max(len(txt1),len(txt2)) - -def calcualte_text_distance(txt1:str,txt2:str) -> float: - """ - Uses a text distance metric to calculate the similarity - between two texts. This is not a sub-string match but a - comparison of similar terms occurring in both texts. - """ - algs = textdistance.algorithms - return algs.levenshtein.normalized_similarity(txt1, txt2) \ No newline at end of file diff --git a/config/issue-rules.yml b/config/issue-rules.yml deleted file mode 100644 index 19000ce..0000000 --- a/config/issue-rules.yml +++ /dev/null @@ -1,15 +0,0 @@ -# list of primary rules -rules: -- contains: question - addLabels: ['question'] -- contains: bug - addLabels: ['bug'] -- contains: feature - addLabels: ['enhancement'] -- contains: enpm611 - assign: ['enpm611'] - -# List that always runs after rules and nomatches. Look for missing sets of tags here. -tags: -- noneIn: ['bug', 'enhancement', 'question'] # If no bug, enhancement, or question labels are added, label with 'triage' - addLabels: ['invalid'] diff --git a/deployment_scripts/update_release_notes.py b/deployment_scripts/update_release_notes.py new file mode 100644 index 0000000..20e6112 --- /dev/null +++ b/deployment_scripts/update_release_notes.py @@ -0,0 +1,50 @@ + + +import os +import re +from datetime import datetime +from pathlib import Path + + +def get_current_version() -> str: + # Read version file + VERSION_FILE = Path("__version__.py") + version_text = VERSION_FILE.read_text() + # Extract current version string + match = re.search(r"__version__\s*=\s*['\"](\d+\.\d+\.\d+)['\"]", version_text) + if not match: + raise ValueError("Could not find __version__ in file.") + return match.group(1) + + +def update_release_notes() -> None: + + # Read the environment variables that were passed in from + # the GitHub Action workflow. + pr_title: str = os.environ.get("PR_TITLE", "").strip() + pr_body: str = os.environ.get("PR_BODY", "").strip() + # Check for missing environment variables + if not pr_title: + raise ValueError("PR_TITLE environment variable is missing.") + + # Get current version from version file + version: str = get_current_version() + + # Create release note entry + date_str: str = datetime.utcnow().strftime("%Y-%m-%d") + entry_lines: list[str] = [ + "", + f"## Release Notes β v{version} β {pr_title} ({date_str})", + "", + pr_body, + "", + ] + + # Append to README + with open("README.md", "a", encoding="utf-8") as f: + f.write("\n".join(entry_lines)) + + print("README updated with release notes.") + +if __name__ == "__main__": + update_release_notes() diff --git a/deployment_scripts/update_version_number.py b/deployment_scripts/update_version_number.py new file mode 100644 index 0000000..7ad249a --- /dev/null +++ b/deployment_scripts/update_version_number.py @@ -0,0 +1,70 @@ + +""" +Bumps the version number in the `__version__.py` file when a Pull +Request is created. +""" + + +import re +import os +from pathlib import Path +from typing import Any, Literal + + +def get_current_version(version_text:str) -> str: + # Extract current version string + match = re.search(r"__version__\s*=\s*['\"](\d+\.\d+\.\d+)['\"]", version_text) + if not match: + raise ValueError("Could not find __version__ in file.") + return match.group(1) + +def get_release_type() -> Literal["patch", "minor", "major"]: + """ Determine the type of release based on the PR title """ + + # Read release title from env + pr_title: str = os.environ.get("PR_TITLE", "").strip() + # Determine release type based on terms in title + if "minor" in pr_title.lower(): + return "minor" + elif "major" in pr_title.lower(): + return "major" + else: + return "patch" + +def bump_version(): + + # Read version file + VERSION_FILE = Path("__version__.py") + version_text = VERSION_FILE.read_text() + + old_version: str = get_current_version(version_text) + major, minor, patch = map(int, old_version.split(".")) + + # Increment based on the requested level + level: Literal['patch', 'minor', 'major'] = get_release_type() + if level == "patch": + patch += 1 + elif level == "minor": + minor += 1 + patch = 0 + elif level == "major": + major += 1 + minor = 0 + patch = 0 + else: + raise ValueError("level must be 'major', 'minor', or 'patch'") + + new_version = f"{major}.{minor}.{patch}" + + # Replace the version string in the file + new_text = version_text.replace(old_version,new_version) + + VERSION_FILE.write_text(new_text) + print(f"Version bumped to {new_version}") + return new_version + + + + +if __name__ == "__main__": + bump_version() diff --git a/docs/github_actions_exercises.svg b/docs/github_actions_exercises.svg new file mode 100644 index 0000000..c3b1d5e --- /dev/null +++ b/docs/github_actions_exercises.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 282786f..f9ddc40 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1 @@ -pytest==8.3.3 -textdistance==4.6.3 \ No newline at end of file +pytest==8.3.3 \ No newline at end of file diff --git a/spacedelivery/delivery.py b/spacedelivery/delivery.py new file mode 100644 index 0000000..07394dd --- /dev/null +++ b/spacedelivery/delivery.py @@ -0,0 +1,69 @@ +import math +from typing import Callable + + +class DeliveryMode: + NORMAL = 1 + TURBO = 2 + HYPERJUMP = 3 + + +MODE_SPEED = { + DeliveryMode.NORMAL: 10, # light-minutes per hour + DeliveryMode.TURBO: 25, + DeliveryMode.HYPERJUMP: 100, +} + +# Planet distances from βGalactic Pizza Hubβ in light-minutes +PLANET_DISTANCE = { + "Mercury": 3, + "Venus": 2, + "Earth": 0.5, + "Mars": 4, + "Jupiter": 25, + "Saturn": 50, + "Neptune": 200, +} + + +def estimate_delivery_time( + planet: str, + mode: int = DeliveryMode.NORMAL, + surge_load: float = 1.0, + weather_delay_fn: Callable[[], float] = lambda: 0.0, +) -> float: + """ + Estimate total delivery time in hours. + + - `planet`: destination planet name + - `mode`: delivery mode speed multiplier + - `surge_load`: factor (>=1); simulates high-order load + - `weather_delay_fn`: returns number of hours to add (stochastic) + """ + + if planet not in PLANET_DISTANCE: + raise ValueError(f"Unknown destination: {planet}") + + if surge_load < 1: + raise ValueError("surge_load must be >= 1") + + if mode not in MODE_SPEED: + raise ValueError("Invalid delivery mode") + + distance = PLANET_DISTANCE[planet] # light-minutes + + # Base time + speed = MODE_SPEED[mode] + travel_time = distance / speed + + # If distance is extremely large, apply nonlinear fatigue penalty + if distance > 100: + travel_time *= 1.2 # 20% fatigue penalty + + # Weather delay (provided by injected function) + weather_delay = weather_delay_fn() + if weather_delay < 0: + raise ValueError("weather_delay_fn returned negative delay") + + total_time = (travel_time * surge_load) + weather_delay + return round(total_time, 2) diff --git a/spacedelivery/web/estimator.js b/spacedelivery/web/estimator.js new file mode 100644 index 0000000..d456db9 --- /dev/null +++ b/spacedelivery/web/estimator.js @@ -0,0 +1,70 @@ +const PLANET_DISTANCE = { + Mercury: 3, + Venus: 2, + Earth: 0.5, + Mars: 4, + Jupiter: 25, + Saturn: 50, + Neptune: 200, +}; + +const MODE_SPEED = { + 1: 10, // NORMAL + 2: 25, // TURBO + 3: 100, // HYPERJUMP +}; + +// Populate planet dropdown +window.onload = () => { + const select = document.getElementById("planet"); + Object.keys(PLANET_DISTANCE).forEach((planet) => { + const option = document.createElement("option"); + option.value = planet; + option.textContent = planet; + select.appendChild(option); + }); +}; + +function estimateDeliveryTime(planet, mode, surgeLoad, weatherDelay) { + if (!(planet in PLANET_DISTANCE)) { + throw new Error("Unknown destination"); + } + if (!(mode in MODE_SPEED)) { + throw new Error("Invalid delivery mode"); + } + if (surgeLoad < 1) { + throw new Error("surgeLoad must be >= 1"); + } + if (weatherDelay < 0) { + throw new Error("weatherDelay must be >= 0"); + } + + const distance = PLANET_DISTANCE[planet]; + const speed = MODE_SPEED[mode]; + + let travelTime = distance / speed; + + // Fatigue penalty for extreme distances + if (distance > 100) { + travelTime *= 1.2; + } + + const total = travelTime * surgeLoad + weatherDelay; + return Math.round(total * 100) / 100; +} + +// Wire UI to estimator +document.getElementById("estimate").onclick = () => { + const planet = document.getElementById("planet").value; + const mode = Number(document.getElementById("mode").value); + const surge = Number(document.getElementById("surge").value); + const weather = Number(document.getElementById("weather").value); + + try { + const time = estimateDeliveryTime(planet, mode, surge, weather); + document.getElementById("result").textContent = + `Estimated delivery time: ${time} hours`; + } catch (err) { + document.getElementById("result").textContent = "Error: " + err.message; + } +}; diff --git a/spacedelivery/web/index.html b/spacedelivery/web/index.html new file mode 100644 index 0000000..df4c568 --- /dev/null +++ b/spacedelivery/web/index.html @@ -0,0 +1,39 @@ + + +
+ + +