Skip to content

Commit d52aba3

Browse files
feat: implement unified full-SHA artifact reference strategy
- Use full 40-character Git SHAs across all layers (build, tag, deploy) - Add versions.yaml as source of truth for artifact versions - Fix container entrypoint to use direct store path - Update promotion workflow to verify artifacts before deploy - Ensure explicit tagging of containers with full SHA after build
1 parent d8fe15d commit d52aba3

File tree

4 files changed

+76
-24
lines changed

4 files changed

+76
-24
lines changed

.github/workflows/pipeline.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ jobs:
5656
- name: Get Version
5757
id: version
5858
run: |
59-
SHA=$(git rev-parse --short HEAD)
59+
SHA=$(git rev-parse HEAD)
6060
PROJECT="hubspoke-demo-dev-b87d"
6161
REPO_URL="us-central1-docker.pkg.dev/$PROJECT/repo/hubspoke-demo"
6262
echo "sha=$SHA" >> $GITHUB_OUTPUT
@@ -140,7 +140,7 @@ jobs:
140140
- name: Get Version
141141
id: version
142142
run: |
143-
SHA=$(git rev-parse --short HEAD)
143+
SHA=$(git rev-parse HEAD)
144144
BUCKET="hubspoke-demo-dev-nixos-images"
145145
echo "sha=$SHA" >> $GITHUB_OUTPUT
146146
echo "bucket=$BUCKET" >> $GITHUB_OUTPUT

.github/workflows/release-prod.yml

Lines changed: 57 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ on:
44
push:
55
branches: [main]
66
paths:
7-
- 'infra/prod.tfvars'
7+
- 'versions.yaml'
8+
- '.github/workflows/release-prod.yml'
89
workflow_dispatch:
910

1011
# CRITICAL: Concurrency Control
@@ -17,7 +18,7 @@ permissions:
1718
id-token: write
1819

1920
jobs:
20-
# Phase 1: Extract and verify configuration
21+
# Phase 1: Extract and verify configuration from versions.yaml
2122
config:
2223
runs-on: ubuntu-latest
2324
outputs:
@@ -29,31 +30,53 @@ jobs:
2930
steps:
3031
- uses: actions/checkout@v4
3132

32-
- name: Parse Environment Config
33+
- name: Parse versions.yaml and Environment Config
3334
id: parse
3435
run: |
35-
PROJECT_ID=$(grep -E '^\s*project_id\s*=' infra/prod.tfvars | head -1 | sed -E 's/.*=\s*"([^"]+)".*/\1/')
36-
SA=$(grep -E '^\s*service_account\s*=' infra/prod.tfvars | head -1 | sed -E 's/.*=\s*"([^"]+)".*/\1/')
37-
BUCKET=$(grep -E '^\s*state_bucket\s*=' infra/prod.tfvars | head -1 | sed -E 's/.*=\s*"([^"]+)".*/\1/')
38-
VERSION=$(grep -E '^\s*image_version\s*=' infra/prod.tfvars | head -1 | sed -E 's/.*=\s*"([^"]+)".*/\1/')
36+
# Read the SHA from versions.yaml (dev section)
37+
DEV_SHA=$(grep -A 1 "^dev:" versions.yaml | grep "image_version:" | sed 's/.*image_version: *//' | tr -d '" ')
3938
40-
# Derive dev project ID from prod service account
41-
# Extract project ID from service account (e.g., tofu-provisioner@hubspoke-demo-prod-f01c.iam.gserviceaccount.com)
42-
# and replace -prod- with -dev- to get the pattern, then use known dev suffix
43-
PROD_SUFFIX=$(echo "$PROJECT_ID" | grep -oE '[a-f0-9]{4}$')
44-
DEV_PROJECT_ID="hubspoke-demo-dev-b87d"
39+
# Read from prod.tfvars
40+
PROJECT_ID=$(grep -E '^\s*project_id\s*=' infra/prod.tfvars | head -1 | sed -E 's/.*=\s*"([^"+)".*/\1/')
41+
SA=$(grep -E '^\s*service_account\s*=' infra/prod.tfvars | head -1 | sed -E 's/.*=\s*"([^"+)".*/\1/')
42+
BUCKET=$(grep -E '^\s*state_bucket\s*=' infra/prod.tfvars | head -1 | sed -E 's/.*=\s*"([^"+)".*/\1/')
4543
46-
echo "version=$VERSION" >> $GITHUB_OUTPUT
44+
# Derive dev project ID (strip -prod- suffix and add -dev- pattern)
45+
DEV_PROJECT_ID=$(echo "$PROJECT_ID" | sed 's/-prod-[a-f0-9]\{4\}/-dev-b87d/')
46+
47+
echo "version=$DEV_SHA" >> $GITHUB_OUTPUT
4748
echo "service_account=$SA" >> $GITHUB_OUTPUT
4849
echo "state_bucket=$BUCKET" >> $GITHUB_OUTPUT
4950
echo "project_id=$PROJECT_ID" >> $GITHUB_OUTPUT
5051
echo "dev_project_id=$DEV_PROJECT_ID" >> $GITHUB_OUTPUT
5152
5253
echo "📋 Configuration:"
53-
echo " Version: $VERSION"
54+
echo " Version (from versions.yaml): $DEV_SHA"
5455
echo " Prod Project: $PROJECT_ID"
5556
echo " Dev Project: $DEV_PROJECT_ID"
5657
58+
- name: Verify Artifacts Exist in Dev
59+
run: |
60+
VERSION="${{ steps.parse.outputs.version }}"
61+
DEV_PROJECT="${{ steps.parse.outputs.dev_project_id }}"
62+
63+
echo "🔍 Verifying artifacts exist in dev project: $DEV_PROJECT"
64+
echo " Version: $VERSION"
65+
66+
# Check container
67+
if ! gcloud artifacts docker images describe "us-central1-docker.pkg.dev/$DEV_PROJECT/repo/hubspoke-demo:$VERSION" --project=$DEV_PROJECT > /dev/null 2>&1; then
68+
echo "::error::Container image not found: us-central1-docker.pkg.dev/$DEV_PROJECT/repo/hubspoke-demo:$VERSION"
69+
exit 1
70+
fi
71+
echo "✅ Container verified"
72+
73+
# Check GCE image
74+
if ! gcloud storage ls "gs://hubspoke-demo-dev-nixos-images/nixos-image-$VERSION.tar.gz" > /dev/null 2>&1; then
75+
echo "::error::GCE image not found: gs://hubspoke-demo-dev-nixos-images/nixos-image-$VERSION.tar.gz"
76+
exit 1
77+
fi
78+
echo "✅ GCE image verified"
79+
5780
# Phase 2: Promote artifacts from dev to prod
5881
promote:
5982
needs: config
@@ -99,32 +122,46 @@ jobs:
99122
run: tofu init -backend-config="bucket=${{ needs.config.outputs.state_bucket }}"
100123

101124
- name: Tofu Plan
102-
id: plan
103125
working-directory: ./infra
104126
run: |
105127
echo "📋 Running tofu plan..."
106128
tofu plan \
107129
-var-file="prod.tfvars" \
130+
-var="image_version=${{ needs.config.outputs.version }}" \
108131
-no-color \
109132
-out=tfplan
110133
111134
- name: Tofu Apply
112-
id: apply
113135
if: github.ref == 'refs/heads/main'
114136
working-directory: ./infra
115137
run: |
116138
echo "🚀 Applying infrastructure changes..."
117139
tofu apply tfplan -no-color
118140
119-
# NOTE: No automatic rollback on failure - artifacts are preserved
120-
# for retry. Manual cleanup available via workflow_dispatch if needed.
141+
- name: Update versions.yaml Prod Section
142+
if: success()
143+
run: |
144+
VERSION="${{ needs.config.outputs.version }}"
145+
TIMESTAMP=$(date -Iseconds)
146+
147+
# Update versions.yaml with prod deployment info
148+
sed -i "/^prod:/,/^[^ ]/{s/image_version:.*/image_version: \"$VERSION\"/}" versions.yaml
149+
sed -i "/^prod:/,/^[^ ]/{s/promoted_at:.*/promoted_at: \"$TIMESTAMP\"/}" versions.yaml
150+
151+
git config --local user.email "github-actions[bot]@users.noreply.github.com"
152+
git config --local user.name "github-actions[bot]"
153+
git add versions.yaml
154+
git commit -m "docs: update prod version to $VERSION [skip ci]"
155+
git push
156+
121157
- name: Deployment Summary
122158
if: always()
123159
run: |
124160
if [ "${{ steps.apply.outcome }}" == "success" ]; then
125161
echo "✅ Deployment completed successfully"
162+
echo " Version: ${{ needs.config.outputs.version }}"
163+
echo " Prod URL: https://$(gcloud run services describe hubspoke-demo --project=${{ needs.config.outputs.project_id }} --region=us-central1 --format='value(status.url)' | sed 's|https://||')"
126164
else
127165
echo "❌ Deployment failed - artifacts preserved in prod for retry"
128-
echo " Container: us-central1-docker.pkg.dev/${{ needs.config.outputs.project_id }}/repo/hubspoke-demo:${{ needs.config.outputs.version }}"
129-
echo " GCE Image: gs://${{ needs.config.outputs.project_id }}-nixos-images/nixos-image-${{ needs.config.outputs.version }}.tar.gz"
166+
echo " Version: ${{ needs.config.outputs.version }}"
130167
fi

flake.nix

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,9 @@
4141
'';
4242

4343
# Entrypoint script mapped to /bin
44-
# FIX: Escaped pr\$run so Bash doesn't eat the $ variable
44+
# Uses direct store path to Rscript for reliability in containerized environments
4545
entrypoint = pkgs.writeShellScriptBin "start-server" ''
46-
${rEnv}/bin/Rscript -e "pr <- plumber::plumb('/app/plumber.R'); pr\$run(host='0.0.0.0', port=8080)"
46+
exec ${rEnv}/bin/Rscript -e "pr <- plumber::plumb('/app/plumber.R'); pr\$run(host='0.0.0.0', port=8080)"
4747
'';
4848

4949
# Unified container root filesystem

versions.yaml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# Versions Tracking for Hub & Spoke Demo
2+
# This file serves as the source of truth for deployed artifact versions
3+
# It is automatically updated by CI/CD pipelines
4+
5+
dev:
6+
image_version: "d8fe15d3cd8332216c907fc87fa925a70045b8cb"
7+
deployed_at: "2026-02-06T18:30:00Z"
8+
status: "healthy"
9+
cloud_run_revision: "hubspoke-demo-00006-pkd"
10+
11+
prod:
12+
image_version: ""
13+
promoted_at: ""
14+
promoted_from: "dev"
15+
status: "pending"

0 commit comments

Comments
 (0)