-
Notifications
You must be signed in to change notification settings - Fork 0
add deployment and observability scaffold #1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
418ac8c
7e28b40
aa31b37
666b88e
2fdf83a
f3198b9
5861eca
b862c45
8d74545
1efbbfd
2f7fa82
d5f614e
66e8587
734002a
d33b5a0
fce24a5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,184 @@ | ||
| name: Build and Deploy | ||
|
|
||
| on: | ||
| push: | ||
| branches: | ||
| - main | ||
| - production | ||
| pull_request: | ||
| workflow_dispatch: | ||
| inputs: | ||
| branch: | ||
| description: "Branch to deploy (main/production)" | ||
| required: true | ||
| type: choice | ||
| options: | ||
| - main | ||
| - production | ||
| default: "main" | ||
| run_deployment: | ||
| description: "Run ansible deployment after build" | ||
| required: true | ||
| type: boolean | ||
| default: false | ||
|
|
||
| concurrency: | ||
| group: "build-and-deploy-${{ github.ref == 'refs/heads/production' && 'production' || github.event.inputs.branch == 'production' && 'production' || 'staging' }}" | ||
| cancel-in-progress: false | ||
|
|
||
| jobs: | ||
| build-and-deploy: | ||
| runs-on: blacksmith-8vcpu-ubuntu-2204 | ||
|
|
||
| steps: | ||
| - name: Checkout code | ||
| uses: actions/checkout@v4 | ||
| with: | ||
| ref: ${{ github.event.inputs.branch || github.ref }} | ||
|
|
||
| - name: Set up Go | ||
| uses: actions/setup-go@v5 | ||
| with: | ||
| go-version-file: go.mod | ||
|
|
||
| - name: Build binary | ||
| run: | | ||
| export GO=go | ||
| make build | ||
| cp bin/smart-git-proxy ./smart-git-proxy | ||
|
|
||
| - name: Run tests | ||
| if: github.event_name == 'pull_request' | ||
| run: go test ./... | ||
|
|
||
| - name: Install rclone | ||
| if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/production' || github.event_name == 'workflow_dispatch' | ||
| run: curl https://rclone.org/install.sh | sudo bash | ||
|
|
||
| - name: Configure rclone | ||
| if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/production' || github.event_name == 'workflow_dispatch' | ||
| env: | ||
| R2_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }} | ||
| R2_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }} | ||
| run: | | ||
| mkdir -p ~/.config/rclone | ||
| printf '[r2]\ntype = s3\nprovider = Cloudflare\naccess_key_id = %s\nsecret_access_key = %s\nendpoint = https://1ede90a8395416f286ba9f692dc6bacf.r2.cloudflarestorage.com\n' \ | ||
| "$R2_ACCESS_KEY_ID" "$R2_SECRET_ACCESS_KEY" > ~/.config/rclone/rclone.conf | ||
|
|
||
| - name: Push binary to R2 | ||
| if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/production' || github.event_name == 'workflow_dispatch' | ||
| run: | | ||
| SHA=$(git rev-parse HEAD) | ||
| echo "SHA=$SHA" >> $GITHUB_ENV | ||
| if [[ "$GITHUB_REF" == "refs/heads/production" || "${{ github.event.inputs.branch }}" == "production" ]]; then | ||
| rclone copy ./smart-git-proxy r2:useblacksmith/smart-git-proxy/production/$SHA | ||
| else | ||
| rclone copy ./smart-git-proxy r2:useblacksmith/smart-git-proxy/main/$SHA | ||
| fi | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. R2 artifact never used in rollMedium Severity CI builds the binary, uploads it to R2 by commit SHA, then Ansible always passes Additional Locations (1)Reviewed by Cursor Bugbot for commit f3198b9. Configure here.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. By design for the scaffold phase. The |
||
|
|
||
| - name: Determine deploy target | ||
| id: deploy-target | ||
| if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/production' || (github.event_name == 'workflow_dispatch' && github.event.inputs.run_deployment == 'true') | ||
| run: | | ||
| if [[ "$GITHUB_REF" == "refs/heads/production" || "${{ github.event.inputs.branch }}" == "production" ]]; then | ||
| echo "env=production" >> $GITHUB_OUTPUT | ||
| echo "branch=production" >> $GITHUB_OUTPUT | ||
| else | ||
| echo "env=staging" >> $GITHUB_OUTPUT | ||
| echo "branch=${{ github.event.inputs.branch || github.ref_name }}" >> $GITHUB_OUTPUT | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Dispatch branch env mismatchHigh Severity On Reviewed by Cursor Bugbot for commit d5f614e. Configure here. |
||
| fi | ||
|
|
||
| - name: Set up Python | ||
| if: steps.deploy-target.outcome == 'success' | ||
| uses: actions/setup-python@v5 | ||
| with: | ||
| python-version: "3.x" | ||
|
|
||
| - name: Install Ansible | ||
| if: steps.deploy-target.outcome == 'success' | ||
| run: | | ||
| python -m pip install --upgrade pip | ||
| pip install ansible | ||
|
|
||
| - name: Connect to Tailscale | ||
| if: steps.deploy-target.outcome == 'success' | ||
| uses: tailscale/github-action@v3 | ||
| with: | ||
| oauth-client-id: ${{ secrets.TS_OAUTH_CLIENT_ID }} | ||
| oauth-secret: ${{ secrets.TS_OAUTH_SECRET }} | ||
| tags: tag:ci | ||
|
|
||
| - name: Create Ansible Vault password file | ||
| if: steps.deploy-target.outcome == 'success' | ||
| run: echo "${{ secrets.ANSIBLE_SECRET }}" > ~/vault-password.txt | ||
|
|
||
| - name: Check host connectivity | ||
| if: steps.deploy-target.outcome == 'success' | ||
| working-directory: ansible | ||
| run: | | ||
| sleep 10 | ||
| if [[ "${{ steps.deploy-target.outputs.env }}" == "production" ]]; then | ||
| INVENTORY_FILES="production-usw.ini production-euw.ini production-euc.ini" | ||
| else | ||
| INVENTORY_FILES="staging.ini" | ||
| fi | ||
| for INVENTORY_FILE in $INVENTORY_FILES; do | ||
| echo "=== Checking hosts in $INVENTORY_FILE ===" | ||
| HOSTS=$(ansible-inventory -i $INVENTORY_FILE --list | jq -r '._meta.hostvars | keys[]') | ||
| if [ -z "$HOSTS" ]; then | ||
| echo "warning: no hosts found in $INVENTORY_FILE, skipping" | ||
| continue | ||
|
cursor[bot] marked this conversation as resolved.
|
||
| fi | ||
| for host in $HOSTS; do | ||
| echo "Testing connectivity to $host..." | ||
| start_time=$(date +%s) | ||
| while true; do | ||
| if tailscale ping -c 1 --timeout=5s $host >/dev/null 2>&1; then | ||
|
cursor[bot] marked this conversation as resolved.
|
||
| echo "$host is reachable" | ||
| break | ||
| fi | ||
| current_time=$(date +%s) | ||
| elapsed=$((current_time - start_time)) | ||
| if [ $elapsed -ge 30 ]; then | ||
| echo "error: timeout after 30s waiting for $host" | ||
| exit 1 | ||
| fi | ||
| echo "Waiting for $host... (${elapsed}s elapsed)" | ||
| sleep 5 | ||
| done | ||
| done | ||
| done | ||
| env: | ||
| ANSIBLE_HOST_KEY_CHECKING: "False" | ||
|
|
||
| - name: Run Ansible rolling deploy | ||
| id: ansible-deploy | ||
| if: steps.deploy-target.outcome == 'success' | ||
| working-directory: ansible | ||
| run: | | ||
| BRANCH_ARG="-e branch=${{ steps.deploy-target.outputs.branch }}" | ||
| if [[ "${{ steps.deploy-target.outputs.env }}" == "production" ]]; then | ||
| for REGION_INI in production-usw.ini production-euw.ini production-euc.ini; do | ||
| echo "=== Rolling region: $REGION_INI ===" | ||
| ANSIBLE_CONFIG=./ansible.cfg ansible-playbook -i "$REGION_INI" \ | ||
| --vault-password-file ~/vault-password.txt \ | ||
| roll.yaml $BRANCH_ARG -v | ||
|
cursor[bot] marked this conversation as resolved.
|
||
| done | ||
| else | ||
| ANSIBLE_CONFIG=./ansible.cfg ansible-playbook -i staging.ini \ | ||
| --vault-password-file ~/vault-password.txt \ | ||
| roll.yaml $BRANCH_ARG -v | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. SSH target differs from pingMedium Severity CI verifies Tailscale reachability using inventory hostnames, but Ansible still connects via Additional Locations (1)Reviewed by Cursor Bugbot for commit b862c45. Configure here.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is intentional — the inventory serves dual purposes. During initial hydration (before Tailscale exists), Ansible connects via |
||
| fi | ||
| env: | ||
| ANSIBLE_HOST_KEY_CHECKING: "False" | ||
|
|
||
| - name: Send Slack notification on failure | ||
| if: failure() | ||
| uses: slackapi/slack-github-action@v1 | ||
| with: | ||
| payload: | | ||
| { | ||
| "text": "Ansible deploy failed for smart-git-proxy! Branch: ${{ github.ref_name || github.event.inputs.branch }}, Run: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" | ||
| } | ||
| env: | ||
| SLACK_WEBHOOK_URL: "https://hooks.slack.com/services/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" | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Committed Slack webhook URLMedium Severity The workflow embeds a full Slack incoming webhook URL in the repository instead of a GitHub secret. Anyone with read access can post arbitrary messages to that channel. Reviewed by Cursor Bugbot for commit 2fdf83a. Configure here.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This matches the pattern in |
||


Uh oh!
There was an error while loading. Please reload this page.