%%{init: {'theme': 'base', 'look': 'classic', 'themeVariables': {'fontFamily': 'monospace'}}}%%
graph TD
Push(["<strong>Developer</strong><br/><em>Pushes to branch</em>"])
GHA["<strong>GitHub Actions</strong>"]
Push --Triggers--> GHA
GHA --"Build Models"--> Build("<strong>Vetiver Models</strong><br/><em>(py_model.qmd, <br>r_model.qmd)</em>")
Build --> Auth("<strong>Configure</strong><br><em>AWS Credentials</em>")
Auth --"Upload to"--> S3[("<strong>S3</strong><br/><em>(using pins)</em>")]
S3 --> Success(["Models<br/>Published"])
style Push fill:#5B8C5A,stroke:#000000,stroke-width:1px,color:#ffffff
style GHA fill:#E8A33D,stroke:#000000,stroke-width:1px,color:#ffffff
style Build fill:#1B2A41,stroke:#000000,stroke-width:1px,color:#ffffff
style Auth fill:#2A6F77,stroke:#000000,stroke-width:1px,color:#ffffff
style S3 fill:#D2562B,stroke:#000000,stroke-width:1px,color:#ffffff
style Success fill:#5B8C5A,stroke:#000000,stroke-width:1px,color:#ffffff
GitHub Actions and S3
GitHub Actions can automatically build and publish our vetiver models to our S3 whenever we push changes to a specific branch (we covered these in Lab 5 and GitHub Actions as CI/CD). I’ve stored the Quarto files for building, pushing, and pulling the model data in the _labs/lab07/ folder.
In this section I’ll cover setting up AWS credentials as repository secrets and creating a workflow to automate the model publishing process.
Automating Model Deployment with GitHub Actions
We need to add AWS configuration to the GitHub repository (Settings > Secrets and variables > Actions). Sensitive credentials go in Secrets, while non-sensitive configuration goes in Variables:
Secrets:
AWS_ACCESS_KEY_ID: AWS access key ID from themjfrigaard-cliIAM user
AWS_SECRET_ACCESS_KEY: AWS secret access key
Variables:
AWS_REGION: AWS region (e.g.,us-east-1)
These credentials belong to the mjfrigaard-cli IAM user with AmazonS3FullAccess permissions (same user for local AWS CLI configuration).
Setting up GitHub Secrets and Variables
The diagram below shows how secrets and variables reach the workflow job.
%%{init: {'theme': 'base', 'look': 'classic', 'themeVariables': {'fontFamily': 'monospace'}}}%%
graph TD
Secrets(["GitHub Secrets<br/>(Sensitive)"])
Vars(["GitHub Variables<br/>(Config)"])
Secrets --> AccessKey("<strong>AWS_ACCESS_KEY_ID</strong>")
Secrets --> SecretKey("<strong>AWS_SECRET_ACCESS_KEY</strong>")
Vars --> Region("<strong>AWS_REGION</strong>")
AccessKey --> Workflow("Workflow Job<br/>(aws-actions/<br>configure-aws-<br>credentials)")
SecretKey --> Workflow
Region --> Workflow
Workflow --> S3("Temporary AWS<br/>Credentials")
S3 --> Access[("Access to S3")]
style Secrets fill:#D2562B,stroke:#000000,stroke-width:1px,color:#ffffff
style Vars fill:#2A6F77,stroke:#000000,stroke-width:1px,color:#ffffff
style AccessKey fill:#1B2A41,stroke:#000000,stroke-width:1px,font-size:14px,text-align:center,font-family:monospace,color:#ffffff
style SecretKey fill:#1B2A41,stroke:#000000,stroke-width:1px,font-size:14px,text-align:center,font-family:monospace,color:#ffffff
style Region fill:#5B8C5A,stroke:#000000,stroke-width:1px,font-size:14px,text-align:center,font-family:monospace,color:#ffffff
style Workflow fill:#E8A33D,stroke:#000000,stroke-width:1px,color:#ffffff
style S3 fill:#2A6F77,stroke:#000000,stroke-width:1px,color:#ffffff
style Access fill:#5B8C5A,stroke:#000000,stroke-width:1px,color:#ffffff
GitHub Actions Workflow File
Next, we create .github/workflows/publish.yml1 to build and publish the models to our S3 bucket. The models are automatically versioned by vetiver (e.g., 20260710T065220Z-91fdd), so we’re creating a new version in S3 with each workflow run.
name: Publish Models to S3
on:
push:
branches: [lab07-s3]
paths:
- '_labs/lab07/py_model.qmd'
- '_labs/lab07/r_model.qmd'
- '.github/workflows/publish.yml'
jobs:
publish-models:
runs-on: ubuntu-latest
steps:
- name: Check out repository
uses: actions/checkout@v4
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: ${{ vars.AWS_REGION || 'us-east-1' }}
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Set up R
uses: r-lib/actions/setup-r@v2
with:
r-version: '4.4'
use-public-rspm: true
- name: Set up Quarto
uses: quarto-dev/quarto-actions/setup@v2
- name: Install Python dependencies
working-directory: ./_labs/lab07
run: |
python -m pip install --upgrade pip
pip install -r Python/requirements.txt pins vetiver python-dotenv s3fs jupyter "setuptools<81"
- name: Install R dependencies
working-directory: ./_labs/lab07
run: |
R -e "install.packages(c('vetiver', 'pins', 'plumber2', 'paws.storage', 'palmerpenguins', 'duckdb', 'DBI', 'dplyr', 'rmarkdown', 'knitr'))"
- name: Build and publish Python model
working-directory: ./_labs/lab07
env:
USE_S3: 'true'
AWS_REGION: ${{ vars.AWS_REGION || 'us-east-1' }}
PINS_ALLOW_PICKLE_READ: '1'
run: |
quarto render py_model.qmd
- name: Build and publish R model
working-directory: ./_labs/lab07
env:
USE_S3: 'true'
AWS_REGION: ${{ vars.AWS_REGION || 'us-east-1' }}
run: |
quarto render r_model.qmd
- 1
-
Trigger: Runs when
py_model.qmd,r_model.qmd, or this workflow file changes on thelab07-s3branch - 2
-
Checkout: Downloads the latest code from the repository
- 3
-
AWS Auth: Uses
aws-actions/configure-aws-credentialsto load the IAM user’s access keys from encrypted GitHub secrets
- 4
-
Setup: Installs Python 3.12, R 4.4, Quarto, and the required dependencies
- 5
-
Build & Publish: Renders
py_model.qmdandr_model.qmdwith theUSE_S3=trueenvironment variable, which causes the model code to write directly to S3
The workflow renders the same .qmd files we render locally, so there are no separate scripts to keep in sync.
The use-public-rspm: true option in the Set up R step points R at the Posit Public Package Manager, which serves prebuilt Linux binaries. Without it, install.packages() compiles every R package from source, and the first successful run of this workflow took about 25 minutes (most of it in Install R dependencies).
Publishing with GitHub Actions automates the model deployment while keeping the AWS credentials in encrypted repository secrets rather than in the code.
Developing on the lab07-s3 Branch
Because I want the main branch reserved for the Quarto book rendering, I’ll create and check out a lab07-s3 branch from main for model changes (pushing to this branch triggers the model publishing workflow):
git checkout -b lab07-s3And push the new branch to GitHub:
git push -u origin lab07-s3Once on the lab07-s3 branch, I’ll need to follow this process:
Make changes
Make changes to the model files (i.e., _labs/lab07/py_model.qmd or _labs/lab07/r_model.qmd)
Test changes (locally)
To test changes locally, I’ll need to set the environment variables for S3 (or use local for testing):
export USE_S3=false # or true if testing S3 locallyThen render/run the files with the changes
quarto render py_model.qmdOr
quarto render r_model.qmdCommit changes
Commit the changes (be specific about the files):
git add _labs/lab07/py_model.qmd _labs/lab07/r_model.qmd
git commit -m "Update model configuration or training logic"Push changes
Push to the lab07-s3 branch:
git push origin lab07-s3GitHub Actions will automatically detects the push to lab07-s3, build the models using the latest code, and publish the versioned models to S3 with AWS credentials from secrets.
Verify
The GitHub CLI (gh) lets us follow the workflow from the Terminal. First, list the most recent runs of publish.yml. Each row shows the status, the commit message, the branch, the run ID, and how long the run took:
gh run list --workflow publish.yml --limit 2Pass the run ID from that list to gh run watch to follow the run until it finishes. The --exit-status flag makes the command fail when the run fails, so we don’t have to read the output to know the result:
gh run watch <run-id> --exit-statusTo see where the time goes, gh api requests the run’s jobs from the GitHub REST API ({owner} and {repo} are filled in from the current repository). The --jq filter prints each step’s name and how many seconds it took. The Install R dependencies step should take about a minute with use-public-rspm: true (compiling from source took most of the 25 minute first run):
gh api repos/{owner}/{repo}/actions/runs/<run-id>/jobs \
--jq '.jobs[0].steps[] | "\(.name): \(((.completed_at|fromdate)-(.started_at|fromdate)))s"'We can also confirm R downloaded its packages from Package Manager by searching the run’s log for its address (packagemanager.posit.co). The __linux__ part of each URL means Package Manager sends a prebuilt binary for the runner’s Linux version:
gh run view <run-id> --log | grep -m3 "packagemanager.posit.co"Finally, we verify the models were published by listing the files in the bucket. Each run adds a new version folder (named with the date and time, e.g., 20261106T183259Z-6a79d) under Python/models/penguin_lab07_model/ and R/models/penguin_lab07_model/:
# List files in S3
aws s3 ls s3://penguin-vetiver-model-data/ --recursive --profile dev-deployMerge to main
Then switch back to the main branch:
git checkout mainThis will merge the changes in lab07-s3 to main (if the changes also belong in the book).
git merge lab07-s3Push changes to main.
git push origin mainThen publish the book:
quarto publish gh-pagesRecap
The AWS access key ID and secret access key go in the repository’s Secrets, and
AWS_REGIONgoes in Variables. The workflow loads them withaws-actions/configure-aws-credentials, so the credentials never appear in the code.publish.ymlruns on pushes to thelab07-s3branch that changepy_model.qmd,r_model.qmd, or the workflow file itself. It installs Python and R, then renders both model documents withUSE_S3=true, so each run writes a newvetiverversion to S3.Model changes happen on
lab07-s3(make changes, test locally, commit, push, and verify withaws s3 ls), which keepsmainreserved for rendering the book. Changes that also belong in the book get merged back intomain.
Lab 8 moves to the command line and how SSH keys work.
Note that file is stored in the root directory (
.github/workflows/)–where all workflow files belong.↩︎
