├── __init__.py ├── channels.db ├── tests ├── __init__.py ├── mock_channels.db └── test_channel_ops.py ├── functions ├── __init__.py ├── channels.db ├── main.py ├── routes.py ├── channels.json └── operations.py ├── .gitignore ├── Pulumi.yaml ├── README.md ├── github_workflow_example.yml ├── LICENSE └── __main__.py /__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /channels.db: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /functions/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | venv/ 3 | -------------------------------------------------------------------------------- /functions/channels.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArjanCodes/2022-cicd/HEAD/functions/channels.db -------------------------------------------------------------------------------- /tests/mock_channels.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArjanCodes/2022-cicd/HEAD/tests/mock_channels.db -------------------------------------------------------------------------------- /Pulumi.yaml: -------------------------------------------------------------------------------- 1 | name: channel_api 2 | runtime: 3 | name: python 4 | options: 5 | virtualenv: venv 6 | description: A simple YouTube channel API running as a Google Cloud Function. 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # How To Setup Github Actions For CI/CD 2 | 3 | GitHub Actions and Pulumi can help you automate your workflow and speed up your CI/CD process. In today's video, I will show you how to use these tools to automate the process of building, testing, and deploying your code. 4 | 5 | Video link: https://youtu.be/9flcoQ1R0Y4. 6 | -------------------------------------------------------------------------------- /functions/main.py: -------------------------------------------------------------------------------- 1 | from flask.wrappers import Request, Response 2 | from routes import app 3 | 4 | 5 | def channel_api(request: Request) -> Response: 6 | # Create a new app context for the internal app 7 | ctx = app.test_request_context( 8 | path=request.full_path, 9 | method=request.method, 10 | ) 11 | ctx.request = request 12 | ctx.push() 13 | response = app.full_dispatch_request() 14 | ctx.pop() 15 | return response 16 | -------------------------------------------------------------------------------- /functions/routes.py: -------------------------------------------------------------------------------- 1 | from flask import Flask 2 | from flask.wrappers import Response 3 | from operations import get_channel 4 | 5 | # Define an internal Flask app 6 | app = Flask("internal") 7 | 8 | # Define the internal paths, idiomatic Flask definition 9 | @app.route("/channels/", methods=["GET", "POST"]) 10 | def channel(channel_id: str): 11 | return get_channel(channel_id) 12 | 13 | 14 | @app.route("/", methods=["GET"]) 15 | def index(): 16 | return Response("Channel API is running.", status=200) 17 | -------------------------------------------------------------------------------- /tests/test_channel_ops.py: -------------------------------------------------------------------------------- 1 | from functools import partial 2 | 3 | import pytest 4 | 5 | from ..functions.operations import ChannelNotFoundError, get_channel 6 | 7 | MOCK_DB = "tests/mock_channels.db" 8 | get_channel_mock = partial(get_channel, db_path=MOCK_DB) 9 | 10 | 11 | def test_get_channel_success() -> None: 12 | channel = get_channel_mock("arjancodes") 13 | assert channel["name"] == "ArjanCodes" 14 | 15 | 16 | def test_get_channel_fail() -> None: 17 | with pytest.raises(ChannelNotFoundError): 18 | get_channel_mock("arjancodes123") 19 | -------------------------------------------------------------------------------- /functions/channels.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "codestackr", 4 | "name": "codeSTACKr", 5 | "tags": ["web development", "typescript"], 6 | "description": "My tutorials are generally about web development and include coding languages such as HTML, CSS, Sass, JavaScript, and TypeScript." 7 | }, 8 | { 9 | "id": "jackherrington", 10 | "name": "Jack Herrington", 11 | "tags": ["frontend", "technology"], 12 | "description": "Frontend videos from basic to very advanced; tutorials, technology deep dives. You'll love it!" 13 | }, 14 | { 15 | "id": "arjancodes", 16 | "name": "ArjanCodes", 17 | "tags": ["software design", "python"], 18 | "description": "ArjanCodes focuses on helping you become a better software developer." 19 | } 20 | ] -------------------------------------------------------------------------------- /functions/operations.py: -------------------------------------------------------------------------------- 1 | import sqlite3 2 | from pathlib import Path 3 | from typing import Any 4 | 5 | 6 | class ChannelNotFoundError(Exception): 7 | pass 8 | 9 | 10 | def get_channel(channel_id: str, db_path: str = "channels.db") -> dict[str, Any]: 11 | print(db_path) 12 | print(f"Working directory: {Path.cwd()}") 13 | with sqlite3.connect(db_path) as connection: 14 | cursor = connection.cursor() 15 | cursor.execute( 16 | "SELECT * FROM channels WHERE id = ?", 17 | (channel_id,), 18 | ) 19 | channel = cursor.fetchone() 20 | print(channel) 21 | if channel is None: 22 | raise ChannelNotFoundError() 23 | return { 24 | "id": channel[0], 25 | "name": channel[1], 26 | "tags": channel[2].split(","), 27 | "description": channel[3], 28 | } 29 | -------------------------------------------------------------------------------- /github_workflow_example.yml: -------------------------------------------------------------------------------- 1 | name: Pulumi 2 | on: 3 | push: 4 | branches: 5 | - main 6 | jobs: 7 | update: 8 | name: Update Channel API 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v2 12 | 13 | - name: Configure gcloud credentials 14 | uses: "google-github-actions/auth@v1" 15 | with: 16 | credentials_json: ${{ secrets.GCS_SA_KEY }} 17 | 18 | - name: Setup gcloud / gsutil 19 | uses: google-github-actions/setup-gcloud@v1 20 | with: 21 | project_id: ${{ secrets.GCS_PROJECT }} 22 | 23 | - name: Setup Python 24 | uses: actions/setup-python@v4 25 | with: 26 | python-version: 3.10.8 27 | cache: pip 28 | 29 | - name: Install requirements 30 | run: pip install -r requirements.txt 31 | 32 | - name: Run tests 33 | run: python -B -m pytest 34 | 35 | - uses: pulumi/actions@v3 36 | with: 37 | command: up 38 | stack-name: dev 39 | env: 40 | PULUMI_ACCESS_TOKEN: ${{ secrets.PULUMI_ACCESS_TOKEN }} 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 ArjanCodes 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /__main__.py: -------------------------------------------------------------------------------- 1 | import os 2 | import time 3 | 4 | import pulumi 5 | from pulumi_gcp import cloudfunctions, storage 6 | 7 | # File path to where the Cloud Function's source code is located. 8 | PATH_TO_SOURCE_CODE = "./functions" 9 | 10 | # We will store the source code to the Cloud Function in a Google Cloud Storage bucket. 11 | bucket = storage.Bucket("channel_api_bucket", location="US", force_destroy=True) 12 | 13 | # Create the single Cloud Storage object, which contains all of the function's 14 | # source code. ("main.py" and "requirements.txt".) 15 | source_archive_object = storage.BucketObject( 16 | "channel_api", 17 | bucket=bucket.name, 18 | source=pulumi.asset.FileArchive(PATH_TO_SOURCE_CODE), 19 | ) 20 | 21 | # Create the Cloud Function, deploying the source we just uploaded to Google 22 | # Cloud Storage. 23 | fxn = cloudfunctions.Function( 24 | "channel_api", 25 | entry_point="channel_api", 26 | runtime="python310", 27 | source_archive_bucket=bucket.name, 28 | source_archive_object=source_archive_object.name, 29 | trigger_http=True, 30 | ) 31 | 32 | invoker = cloudfunctions.FunctionIamMember( 33 | "invoker", 34 | project=fxn.project, 35 | region=fxn.region, 36 | cloud_function=fxn.name, 37 | role="roles/cloudfunctions.invoker", 38 | member="allUsers", 39 | ) 40 | 41 | # Export the DNS name of the bucket and the cloud function URL. 42 | pulumi.export("bucket_name", bucket.url) 43 | pulumi.export("fxn_url", fxn.https_trigger_url) 44 | --------------------------------------------------------------------------------