Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 76 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
name: Deploy

on:
push:
branches:
- master
workflow_dispatch:

permissions:
contents: read
id-token: write

concurrency:
group: deploy-production
cancel-in-progress: false

jobs:
deploy:
runs-on: ubuntu-latest
environment: production
env:
GCP_PROJECT_ID: ${{ vars.GCP_PROJECT_ID }}
GCP_REGION: ${{ vars.GCP_REGION || 'us-central1' }}
GCP_FUNCTION_NAME: ${{ vars.GCP_FUNCTION_NAME || 'progress' }}
GCP_RUNTIME: ${{ vars.GCP_RUNTIME || 'go124' }}
GCP_ENTRY_POINT: ${{ vars.GCP_ENTRY_POINT || 'Progress' }}
GCP_WORKLOAD_IDENTITY_PROVIDER: ${{ vars.GCP_WORKLOAD_IDENTITY_PROVIDER }}
GCP_SERVICE_ACCOUNT: ${{ vars.GCP_SERVICE_ACCOUNT }}
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Validate deploy configuration
run: |
set -euo pipefail
required_vars=(
GCP_PROJECT_ID
GCP_WORKLOAD_IDENTITY_PROVIDER
GCP_SERVICE_ACCOUNT
)

for var in "${required_vars[@]}"; do
if [[ -z "${!var:-}" ]]; then
echo "Missing required repository variable: $var"
exit 1
fi
done

- name: Authenticate to Google Cloud
uses: google-github-actions/auth@v3
with:
workload_identity_provider: ${{ env.GCP_WORKLOAD_IDENTITY_PROVIDER }}
service_account: ${{ env.GCP_SERVICE_ACCOUNT }}
project_id: ${{ env.GCP_PROJECT_ID }}

- name: Deploy Cloud Function
id: deploy
uses: google-github-actions/deploy-cloud-functions@v4
with:
name: ${{ env.GCP_FUNCTION_NAME }}
project_id: ${{ env.GCP_PROJECT_ID }}
region: ${{ env.GCP_REGION }}
runtime: ${{ env.GCP_RUNTIME }}
entry_point: ${{ env.GCP_ENTRY_POINT }}
environment: GEN_2
source_dir: ./
all_traffic_on_latest_revision: true

- name: Smoke test deployed endpoint
env:
BASE_URL: ${{ steps.deploy.outputs.url }}
PROGRESS_PATH: ""
run: bash ./scripts/smoke.sh

- name: Print deployed URL
run: echo "Deployed URL: ${{ steps.deploy.outputs.url }}"
107 changes: 107 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,13 @@ BASE_URL=https://geps.dev mise exec -- make smoke

The smoke test validates status codes, headers, and basic content contract.

If your `BASE_URL` already includes the function path (for example
`https://REGION-PROJECT.cloudfunctions.net/progress`), set:

```bash
BASE_URL=https://REGION-PROJECT.cloudfunctions.net/progress PROGRESS_PATH="" mise exec -- make smoke
```

## Deploy (Google Cloud)

Set your project first:
Expand All @@ -135,10 +142,110 @@ After deploy, run smoke tests:
BASE_URL=https://YOUR_DOMAIN_OR_FUNCTION_URL mise exec -- make smoke
```

## Automated Deploy (GitHub Actions -> GCP)

This repo includes `.github/workflows/deploy.yml` to deploy automatically on
push to `master` (and manually via `workflow_dispatch`).

### 1) Configure GitHub repository variables

Required:

- `GCP_PROJECT_ID` (example: `progress-markdown`)
- `GCP_WORKLOAD_IDENTITY_PROVIDER` (full resource name)
- `GCP_SERVICE_ACCOUNT` (deployer service account email)

Optional (defaults are already set in workflow):

- `GCP_REGION` (`us-central1`)
- `GCP_FUNCTION_NAME` (`progress`)
- `GCP_RUNTIME` (`go124`)
- `GCP_ENTRY_POINT` (`Progress`)

### 2) One-time GCP setup (OIDC/WIF, no JSON keys)

Create deployer service account:

```bash
gcloud iam service-accounts create github-deployer \
--display-name "GitHub deployer for markdown-progress"
```

Grant deploy permissions on project:

```bash
PROJECT_ID=progress-markdown
PROJECT_NUMBER=$(gcloud projects describe "$PROJECT_ID" --format='value(projectNumber)')
SA="github-deployer@${PROJECT_ID}.iam.gserviceaccount.com"

gcloud projects add-iam-policy-binding "$PROJECT_ID" \
--member="serviceAccount:${SA}" \
--role="roles/cloudfunctions.developer"

gcloud projects add-iam-policy-binding "$PROJECT_ID" \
--member="serviceAccount:${SA}" \
--role="roles/run.admin"

gcloud projects add-iam-policy-binding "$PROJECT_ID" \
--member="serviceAccount:${SA}" \
--role="roles/artifactregistry.writer"

gcloud projects add-iam-policy-binding "$PROJECT_ID" \
--member="serviceAccount:${SA}" \
--role="roles/cloudbuild.builds.editor"

gcloud projects add-iam-policy-binding "$PROJECT_ID" \
--member="serviceAccount:${SA}" \
--role="roles/iam.serviceAccountUser"
```

Create Workload Identity Pool + Provider:

```bash
PROJECT_ID=progress-markdown
PROJECT_NUMBER=$(gcloud projects describe "$PROJECT_ID" --format='value(projectNumber)')
POOL_ID=github
PROVIDER_ID=github-oidc

gcloud iam workload-identity-pools create "$POOL_ID" \
--project="$PROJECT_ID" \
--location="global" \
--display-name="GitHub Actions Pool"

gcloud iam workload-identity-pools providers create-oidc "$PROVIDER_ID" \
--project="$PROJECT_ID" \
--location="global" \
--workload-identity-pool="$POOL_ID" \
--display-name="GitHub Actions Provider" \
--issuer-uri="https://token.actions.githubusercontent.com" \
--attribute-mapping="google.subject=assertion.sub,attribute.repository=assertion.repository,attribute.ref=assertion.ref"
```

Allow only this repository to impersonate the deployer service account:

```bash
PROJECT_ID=progress-markdown
PROJECT_NUMBER=$(gcloud projects describe "$PROJECT_ID" --format='value(projectNumber)')
POOL_ID=github
REPO="gepser/markdown-progress"
SA="github-deployer@${PROJECT_ID}.iam.gserviceaccount.com"

gcloud iam service-accounts add-iam-policy-binding "$SA" \
--role="roles/iam.workloadIdentityUser" \
--member="principalSet://iam.googleapis.com/projects/${PROJECT_NUMBER}/locations/global/workloadIdentityPools/${POOL_ID}/attribute.repository/${REPO}"
```

Provider resource name to set in GitHub variable:

```text
projects/PROJECT_NUMBER/locations/global/workloadIdentityPools/POOL_ID/providers/PROVIDER_ID
```

## CI

- `CI` workflow runs `go test` and `go vet` on pushes and PRs.
- `Smoke Tests` workflow can be run manually (`workflow_dispatch`) with a `base_url` input.
- `Deploy` workflow deploys to GCP on `master` using OIDC/WIF.

## Contributing

Expand Down
33 changes: 24 additions & 9 deletions scripts/smoke.sh
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,21 @@ if [[ -z "${BASE_URL:-}" ]]; then
fi

BASE_URL="${BASE_URL%/}"
PROGRESS_PATH="${PROGRESS_PATH:-/progress}"

if [[ -n "$PROGRESS_PATH" && "$PROGRESS_PATH" != /* ]]; then
PROGRESS_PATH="/${PROGRESS_PATH}"
fi

progress_url() {
local suffix="$1"
if [[ -z "$PROGRESS_PATH" ]]; then
echo "${BASE_URL}${suffix}"
return
fi

echo "${BASE_URL}${PROGRESS_PATH}${suffix}"
}

expect_status() {
local expected="$1"
Expand Down Expand Up @@ -60,16 +75,16 @@ expect_body_contains() {

echo "Running smoke tests against ${BASE_URL}"

expect_status "200" "GET" "${BASE_URL}/progress/76"
expect_header_contains "${BASE_URL}/progress/76" "Content-Type" "image/svg+xml"
expect_header_contains "${BASE_URL}/progress/76" "Cache-Control" "max-age=300"
expect_body_contains "${BASE_URL}/progress/76" "76%"
expect_status "200" "GET" "$(progress_url "/76")"
expect_header_contains "$(progress_url "/76")" "Content-Type" "image/svg+xml"
expect_header_contains "$(progress_url "/76")" "Cache-Control" "max-age=300"
expect_body_contains "$(progress_url "/76")" "76%"

expect_status "400" "GET" "${BASE_URL}/progress/not-a-number"
expect_status "400" "GET" "${BASE_URL}/progress/50?successColor=nothex"
expect_status "405" "POST" "${BASE_URL}/progress/50"
expect_status "400" "GET" "$(progress_url "/not-a-number")"
expect_status "400" "GET" "$(progress_url "/50?successColor=nothex")"
expect_status "405" "POST" "$(progress_url "/50")"

expect_status "200" "GET" "${BASE_URL}/progress/150"
expect_body_contains "${BASE_URL}/progress/150" "100%"
expect_status "200" "GET" "$(progress_url "/150")"
expect_body_contains "$(progress_url "/150")" "100%"

echo "Smoke tests passed."