Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
a76ddc2
Add migration script to populate NMBGMR site names as alternate IDs
jeremyzilar May 1, 2026
e669892
Formatting changes
jeremyzilar May 1, 2026
a6b937b
Fall back to organization name in contact search results
jeremyzilar May 1, 2026
48305f2
Use LocationId/nma_pk_location for NMBGMR site name matching
jeremyzilar May 1, 2026
85e626e
Merge pull request #669 from DataIntegrationGroup/BDMS-786-Company-Co…
jeremyzilar May 1, 2026
55f593c
Merge pull request #668 from DataIntegrationGroup/BDMS-787-site_name-…
jeremyzilar May 1, 2026
2be9fbd
feat(lexicon): add new organization terms
ksmuczynski May 1, 2026
5f8ba47
Merge pull request #670 from DataIntegrationGroup/kas-add-new-organiz…
ksmuczynski May 4, 2026
9b32896
build(deps): bump python-multipart from 0.0.26 to 0.0.27 (#672)
dependabot[bot] May 8, 2026
a0240bd
build(deps): bump mako from 1.3.11 to 1.3.12 (#673)
dependabot[bot] May 8, 2026
70fb779
build(deps): bump urllib3 from 2.6.3 to 2.7.0 (#674)
dependabot[bot] May 11, 2026
f1d07e3
docs(.env.example): Add required env
TylerAdamMartinez May 11, 2026
ff86895
build(deps): bump authlib from 1.6.11 to 1.6.12 (#676)
dependabot[bot] May 13, 2026
95a28a5
docs(env.example): Add PYGEOAPI_POSTGRES_USER
TylerAdamMartinez May 18, 2026
bf3e0eb
Merge pull request #675 from DataIntegrationGroup/TAM-no-ticket-add-env
TylerAdamMartinez May 18, 2026
6f9a710
build(deps): bump idna from 3.11 to 3.15 (#677)
dependabot[bot] May 19, 2026
c6164bb
feat(lexicon): add new organization terms for Santa Ana Pueblo, Villa…
ksmuczynski May 26, 2026
69fb435
Merge pull request #678 from DataIntegrationGroup/kas-lexicon-update-…
ksmuczynski May 26, 2026
50647eb
build(deps): bump actions/checkout in the gha-minor-and-patch group (…
dependabot[bot] Jun 2, 2026
f0e9ec9
chore: adopt Data Services Versioning Standard (#680)
jirhiker Jun 3, 2026
821d25d
build(deps): bump aiohttp from 3.13.4 to 3.14.0 (#684)
dependabot[bot] Jun 3, 2026
f9e538c
feat: add POST /asset/upload-and-record endpoint for digital asset up…
likithabommasani21 Jun 4, 2026
0586b56
build(deps): bump virtualenv from 20.32.0 to 21.2.0 (#629)
dependabot[bot] Jun 7, 2026
afd0ed7
build(deps): bump importlib-metadata from 8.7.1 to 9.0.0 (#628)
dependabot[bot] Jun 7, 2026
11c1f2c
build(deps): bump astral-sh/setup-uv from 8.1.0 to 8.2.0 in the gha-m…
dependabot[bot] Jun 8, 2026
9063c4b
build(deps): bump the uv-non-major group with 51 updates (#691)
dependabot[bot] Jun 8, 2026
5526f70
build(deps): bump amannn/action-semantic-pull-request from 5 to 6 (#688)
dependabot[bot] Jun 8, 2026
e4276a9
build(deps): bump googleapis/release-please-action from 4 to 5 (#690)
dependabot[bot] Jun 8, 2026
bc8165e
chore: merge production into staging (#696)
jirhiker Jun 8, 2026
f47d4a9
chore: merge production into staging (preserve merge commit) (#697)
jirhiker Jun 8, 2026
fb847f8
fix: validate thing_id before GCS upload to prevent orphaned blobs (#…
jirhiker Jun 8, 2026
d565dd0
Merge remote-tracking branch 'origin/production' into staging
jirhiker Jun 8, 2026
c394ca5
ci: trigger CD (Production) on release publish, not tag push
jirhiker Jun 8, 2026
f446557
Merge pull request #699 from DataIntegrationGroup/merge/production-in…
jirhiker Jun 8, 2026
ca546e3
ci: gate CD (Production) to version-shaped release tags
jirhiker Jun 8, 2026
7e88172
ci: use refs/tags/ prefix when checking out the release tag
jirhiker Jun 8, 2026
e063ed2
Merge pull request #700 from DataIntegrationGroup/ci/cd-production-on…
jirhiker Jun 8, 2026
46ca905
test: replace removed CliRunner.isolated_filesystem with tmp_path (#703)
jirhiker Jun 8, 2026
ff59ea0
ci: reindent CD workflows + asset upload orphan-blob fixes (#702)
jirhiker Jun 8, 2026
018cc44
fix: pin joserfc in requirements.txt to unblock deploy (#704)
jirhiker Jun 8, 2026
3892047
fix: move pre-commit/pytest dev tools out of prod deps (unblock deplo…
jirhiker Jun 8, 2026
9c55e2e
build(deps): bump ecdsa from 0.19.1 to 0.19.2 (#706)
dependabot[bot] Jun 8, 2026
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
291 changes: 152 additions & 139 deletions .github/workflows/CD_production.yml
Original file line number Diff line number Diff line change
@@ -1,147 +1,160 @@
name: CD (Production)

on:
push:
tags:
- 'v*.*.*' # GA releases: v1.0.0, v1.4.2
- 'v*.*.*-*' # SemVer pre-releases: v1.0.0-rc.1
- 'v*.*.*[a-z]*' # PEP 440 pre-releases: v1.0.0rc1, v1.0.0b2 (release-please-python form)
release:
types: [published]

permissions:
contents: read

jobs:
production-deploy:

runs-on: ubuntu-latest
environment: production

steps:
- name: Check out source repository
uses: actions/checkout@v6.0.3
with:
fetch-depth: 0

- name: Install uv in container
uses: astral-sh/setup-uv@v8.1.0
with:
version: "latest"

- name: Generate requirements.txt
run: |
uv export \
--format requirements-txt \
--no-emit-project \
--no-dev \
--output-file requirements.txt

- name: Authenticate to Google Cloud
uses: 'google-github-actions/auth@v3'
with:
credentials_json: ${{ secrets.CLOUD_DEPLOY_SERVICE_ACCOUNT_KEY }}

- name: Run Alembic migrations on production database
env:
DB_DRIVER: "cloudsql"
CLOUD_SQL_INSTANCE_NAME: "${{ secrets.CLOUD_SQL_INSTANCE_NAME }}"
CLOUD_SQL_DATABASE: "${{ vars.CLOUD_SQL_DATABASE }}"
CLOUD_SQL_USER: "${{ secrets.CLOUD_SQL_USER }}"
CLOUD_SQL_IAM_AUTH: true
run: |
uv run alembic upgrade head

- name: Refresh materialized views on production database
env:
DB_DRIVER: "cloudsql"
CLOUD_SQL_INSTANCE_NAME: "${{ secrets.CLOUD_SQL_INSTANCE_NAME }}"
CLOUD_SQL_DATABASE: "${{ vars.CLOUD_SQL_DATABASE }}"
CLOUD_SQL_USER: "${{ secrets.CLOUD_SQL_USER }}"
CLOUD_SQL_IAM_AUTH: true
run: |
uv run python -m cli.cli refresh-pygeoapi-materialized-views

- name: Ensure envsubst is available
run: |
if ! command -v envsubst >/dev/null 2>&1; then
sudo apt-get update
sudo apt-get install -y gettext-base
fi

- name: Render App Engine configs
env:
APP_VERSION: ${{ github.ref_name }}
ENVIRONMENT: "production"
CLOUD_SQL_INSTANCE_NAME: "${{ secrets.CLOUD_SQL_INSTANCE_NAME }}"
CLOUD_SQL_DATABASE: "${{ vars.CLOUD_SQL_DATABASE }}"
CLOUD_SQL_USER: "${{ secrets.CLOUD_SQL_USER }}"
PYGEOAPI_POSTGRES_DB: "${{ vars.CLOUD_SQL_DATABASE }}"
PYGEOAPI_POSTGRES_USER: "${{ secrets.PYGEOAPI_POSTGRES_USER }}"
PYGEOAPI_POSTGRES_HOST: "${{ vars.PYGEOAPI_POSTGRES_HOST || '127.0.0.1' }}"
PYGEOAPI_POSTGRES_PORT: "${{ vars.PYGEOAPI_POSTGRES_PORT || '5432' }}"
PYGEOAPI_POSTGRES_PASSWORD: "${{ secrets.PYGEOAPI_POSTGRES_PASSWORD }}"
PYGEOAPI_SERVER_URL: "${{ vars.PYGEOAPI_SERVER_URL }}"
CLOUD_SQL_IAM_AUTH: "true"
GCS_SERVICE_ACCOUNT_KEY: "${{ secrets.GCS_SERVICE_ACCOUNT_KEY }}"
GCS_BUCKET_NAME: "${{ vars.GCS_BUCKET_NAME }}"
AUTHENTIK_URL: "${{ vars.AUTHENTIK_URL }}"
AUTHENTIK_CLIENT_ID: "${{ vars.AUTHENTIK_CLIENT_ID }}"
AUTHENTIK_AUTHORIZE_URL: "${{ vars.AUTHENTIK_AUTHORIZE_URL }}"
AUTHENTIK_TOKEN_URL: "${{ vars.AUTHENTIK_TOKEN_URL }}"
SESSION_SECRET_KEY: "${{ secrets.SESSION_SECRET_KEY }}"
APITALLY_CLIENT_ID: "${{ vars.APITALLY_CLIENT_ID }}"
run: |
export MAX_INSTANCES="10"
export SERVICE_NAME="ocotillo-api"
export ENTRYPOINT="gunicorn -w 4 -k uvicorn.workers.UvicornWorker main:app"
export MIN_INSTANCES="0"
envsubst < .github/app.template.yaml > app.yaml

- name: Deploy to Google Cloud
run: |
gcloud app deploy \
app.yaml \
--quiet \
--project ${{ vars.GCP_PROJECT_ID }}

- name: Clean up oldest versions
run: |
SERVICE="ocotillo-api"
VERSIONS_JSON="$(gcloud app versions list --service="$SERVICE" --project=${{ vars.GCP_PROJECT_ID }} --format=json --sort-by="version.createTime" 2>/dev/null || printf '[]')"
export VERSIONS_JSON
DELETE_VERSION="$(python - <<'PY'
import json
import os

versions = json.loads(os.environ.get("VERSIONS_JSON", "[]") or "[]")
if len(versions) <= 1:
print("")
raise SystemExit(0)

def traffic_split(version):
for key in ("traffic_split", "trafficSplit"):
value = version.get(key)
if value is not None:
try:
return float(value)
except (TypeError, ValueError):
return 0.0
return 0.0

for version in versions:
if traffic_split(version) == 0.0:
print(version.get("id", ""))
break
else:
print("")
PY
)"
if [ -n "$DELETE_VERSION" ]; then
echo "Deleting old non-serving version for $SERVICE: $DELETE_VERSION"
gcloud app versions delete "$DELETE_VERSION" --service="$SERVICE" --project=${{ vars.GCP_PROJECT_ID }} --quiet
else
echo "No old non-serving versions to delete for $SERVICE"
fi

- name: Remove rendered configs
run: |
rm app.yaml
production-deploy:
# Safety rail: only deploy when the release tag is version-shaped
# (v*.*.*, v*.*.*-*, v*.*.*[a-z]*). startsWith() is a cheap pre-filter;
# the "Validate release tag" step enforces the strict regex.
if: startsWith(github.event.release.tag_name, 'v')

runs-on: ubuntu-latest
environment: production

steps:
- name: Validate release tag matches version pattern
env:
TAG: ${{ github.event.release.tag_name }}
run: |
if [[ ! "$TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-.+|[a-z].*)?$ ]]; then
echo "Release tag '$TAG' does not match the v*.*.* pattern. Refusing to deploy."
exit 1
fi

- name: Check out source repository
uses: actions/checkout@v6.0.3
with:
fetch-depth: 0
# Fully-qualified tag ref avoids ambiguity if a branch is ever
# created with the same name as the release tag.
ref: refs/tags/${{ github.event.release.tag_name }}

- name: Install uv in container
uses: astral-sh/setup-uv@v8.2.0
with:
version: "latest"

- name: Generate requirements.txt
run: |
uv export \
--format requirements-txt \
--no-emit-project \
--no-dev \
--output-file requirements.txt

- name: Authenticate to Google Cloud
uses: 'google-github-actions/auth@v3'
with:
credentials_json: ${{ secrets.CLOUD_DEPLOY_SERVICE_ACCOUNT_KEY }}

- name: Run Alembic migrations on production database
env:
DB_DRIVER: "cloudsql"
CLOUD_SQL_INSTANCE_NAME: "${{ secrets.CLOUD_SQL_INSTANCE_NAME }}"
CLOUD_SQL_DATABASE: "${{ vars.CLOUD_SQL_DATABASE }}"
CLOUD_SQL_USER: "${{ secrets.CLOUD_SQL_USER }}"
CLOUD_SQL_IAM_AUTH: true
run: |
uv run --no-dev alembic upgrade head

- name: Refresh materialized views on production database
env:
DB_DRIVER: "cloudsql"
CLOUD_SQL_INSTANCE_NAME: "${{ secrets.CLOUD_SQL_INSTANCE_NAME }}"
CLOUD_SQL_DATABASE: "${{ vars.CLOUD_SQL_DATABASE }}"
CLOUD_SQL_USER: "${{ secrets.CLOUD_SQL_USER }}"
CLOUD_SQL_IAM_AUTH: true
run: |
uv run --no-dev python -m cli.cli refresh-pygeoapi-materialized-views

- name: Ensure envsubst is available
run: |
if ! command -v envsubst >/dev/null 2>&1; then
sudo apt-get update
sudo apt-get install -y gettext-base
fi

- name: Render App Engine configs
env:
APP_VERSION: ${{ github.event.release.tag_name }}
ENVIRONMENT: "production"
CLOUD_SQL_INSTANCE_NAME: "${{ secrets.CLOUD_SQL_INSTANCE_NAME }}"
CLOUD_SQL_DATABASE: "${{ vars.CLOUD_SQL_DATABASE }}"
CLOUD_SQL_USER: "${{ secrets.CLOUD_SQL_USER }}"
PYGEOAPI_POSTGRES_DB: "${{ vars.CLOUD_SQL_DATABASE }}"
PYGEOAPI_POSTGRES_USER: "${{ secrets.PYGEOAPI_POSTGRES_USER }}"
PYGEOAPI_POSTGRES_HOST: "${{ vars.PYGEOAPI_POSTGRES_HOST || '127.0.0.1' }}"
PYGEOAPI_POSTGRES_PORT: "${{ vars.PYGEOAPI_POSTGRES_PORT || '5432' }}"
PYGEOAPI_POSTGRES_PASSWORD: "${{ secrets.PYGEOAPI_POSTGRES_PASSWORD }}"
PYGEOAPI_SERVER_URL: "${{ vars.PYGEOAPI_SERVER_URL }}"
CLOUD_SQL_IAM_AUTH: "true"
GCS_SERVICE_ACCOUNT_KEY: "${{ secrets.GCS_SERVICE_ACCOUNT_KEY }}"
GCS_BUCKET_NAME: "${{ vars.GCS_BUCKET_NAME }}"
AUTHENTIK_URL: "${{ vars.AUTHENTIK_URL }}"
AUTHENTIK_CLIENT_ID: "${{ vars.AUTHENTIK_CLIENT_ID }}"
AUTHENTIK_AUTHORIZE_URL: "${{ vars.AUTHENTIK_AUTHORIZE_URL }}"
AUTHENTIK_TOKEN_URL: "${{ vars.AUTHENTIK_TOKEN_URL }}"
SESSION_SECRET_KEY: "${{ secrets.SESSION_SECRET_KEY }}"
APITALLY_CLIENT_ID: "${{ vars.APITALLY_CLIENT_ID }}"
run: |
export MAX_INSTANCES="10"
export SERVICE_NAME="ocotillo-api"
export ENTRYPOINT="gunicorn -w 4 -k uvicorn.workers.UvicornWorker main:app"
export MIN_INSTANCES="0"
envsubst < .github/app.template.yaml > app.yaml

- name: Deploy to Google Cloud
run: |
gcloud app deploy \
app.yaml \
--quiet \
--project ${{ vars.GCP_PROJECT_ID }}

- name: Clean up oldest versions
run: |
SERVICE="ocotillo-api"
VERSIONS_JSON="$(gcloud app versions list --service="$SERVICE" --project=${{ vars.GCP_PROJECT_ID }} --format=json --sort-by="version.createTime" 2>/dev/null || printf '[]')"
export VERSIONS_JSON
DELETE_VERSION="$(python - <<'PY'
import json
import os

versions = json.loads(os.environ.get("VERSIONS_JSON", "[]") or "[]")
if len(versions) <= 1:
print("")
raise SystemExit(0)

def traffic_split(version):
for key in ("traffic_split", "trafficSplit"):
value = version.get(key)
if value is not None:
try:
return float(value)
except (TypeError, ValueError):
return 0.0
return 0.0

for version in versions:
if traffic_split(version) == 0.0:
print(version.get("id", ""))
break
else:
print("")
PY
)"
if [ -n "$DELETE_VERSION" ]; then
echo "Deleting old non-serving version for $SERVICE: $DELETE_VERSION"
gcloud app versions delete "$DELETE_VERSION" --service="$SERVICE" --project=${{ vars.GCP_PROJECT_ID }} --quiet
else
echo "No old non-serving versions to delete for $SERVICE"
fi

- name: Remove rendered configs
run: |
rm app.yaml
Loading