name: issue-branch-preview on: push: branches-ignore: - main workflow_dispatch: inputs: branch: description: "Target branch (optional, default current ref)" required: false type: string issue: description: "Issue number (optional, auto-parse from branch)" required: false type: string jobs: allocate-and-deploy: runs-on: ubuntu-latest env: PREVIEW_SLOTS: ${{ vars.PREVIEW_SLOTS }} PREVIEW_URL_TEMPLATE: ${{ vars.PREVIEW_URL_TEMPLATE }} PREVIEW_TTL_HOURS: ${{ vars.PREVIEW_TTL_HOURS }} PREVIEW_STATE_FILE: .tmp/preview-slots.json CLIENT_DEPLOY_CMD: ${{ vars.CLIENT_DEPLOY_CMD }} SERVER_DEPLOY_CMD: ${{ vars.SERVER_DEPLOY_CMD }} FULL_STACK_DEPLOY_CMD: ${{ vars.FULL_STACK_DEPLOY_CMD }} INFRA_APPLY_CMD: ${{ vars.INFRA_APPLY_CMD }} steps: - name: Checkout uses: actions/checkout@v4 with: fetch-depth: 0 - name: Setup Python uses: actions/setup-python@v5 with: python-version: "3.11" - name: Resolve branch and issue id: target shell: bash run: | BRANCH="${{ inputs.branch }}" ISSUE_INPUT="${{ inputs.issue }}" if [ -z "$BRANCH" ]; then BRANCH="${GITHUB_REF_NAME:-$(git rev-parse --abbrev-ref HEAD)}" fi ISSUE_ID="$ISSUE_INPUT" if [ -z "$ISSUE_ID" ]; then ISSUE_ID="$(echo "$BRANCH" | sed -nE 's#^issue[-/ ]?([0-9]+).*$#\1#p')" fi if [ -z "$ISSUE_ID" ]; then ISSUE_ID="$(echo "$BRANCH" | sed -nE 's#^.*/([0-9]+).*$#\1#p')" fi if [ -z "$ISSUE_ID" ]; then ISSUE_ID="0" fi echo "branch=$BRANCH" >> "$GITHUB_OUTPUT" echo "issue=$ISSUE_ID" >> "$GITHUB_OUTPUT" - name: Detect change scope id: scope shell: bash run: | git fetch origin main --depth=1 || true mkdir -p .tmp python skills/gitea-issue-devops-agent/scripts/change_scope.py \ --repo-path . \ --base-ref origin/main \ --head-ref "${{ steps.target.outputs.branch }}" > .tmp/change-scope.json SCOPE="$(python -c "import json;print(json.load(open('.tmp/change-scope.json', encoding='utf-8'))['scope'])")" echo "scope=$SCOPE" >> "$GITHUB_OUTPUT" cat .tmp/change-scope.json - name: Allocate preview slot id: slot shell: bash run: | SLOTS="${PREVIEW_SLOTS:-preview-a,preview-b}" TTL="${PREVIEW_TTL_HOURS:-24}" URL_TEMPLATE="${PREVIEW_URL_TEMPLATE:-https://{slot}.qa.example.com}" mkdir -p .tmp python skills/gitea-issue-devops-agent/scripts/preview_slot_allocator.py \ --state-file "$PREVIEW_STATE_FILE" \ --slots "$SLOTS" \ --repo "${GITHUB_REPOSITORY}" \ --issue "${{ steps.target.outputs.issue }}" \ --branch "${{ steps.target.outputs.branch }}" \ --ttl-hours "$TTL" \ --url-template "$URL_TEMPLATE" \ --evict-oldest > .tmp/slot-allocation.json SLOT="$(python -c "import json;d=json.load(open('.tmp/slot-allocation.json', encoding='utf-8'));print(d.get('allocation',{}).get('slot',''))")" URL="$(python -c "import json;d=json.load(open('.tmp/slot-allocation.json', encoding='utf-8'));print(d.get('allocation',{}).get('url',''))")" echo "slot=$SLOT" >> "$GITHUB_OUTPUT" echo "url=$URL" >> "$GITHUB_OUTPUT" cat .tmp/slot-allocation.json - name: Deploy by scope shell: bash run: | set -euo pipefail SCOPE="${{ steps.scope.outputs.scope }}" run_or_echo () { local cmd="$1" local fallback="$2" if [ -n "$cmd" ]; then bash -lc "$cmd" else echo "$fallback" fi } case "$SCOPE" in skip) echo "Scope=skip: docs/tests-only or no changes, deployment skipped." ;; client_only) run_or_echo "${CLIENT_DEPLOY_CMD:-}" "Scope=client_only: set repo var CLIENT_DEPLOY_CMD." ;; server_only) run_or_echo "${SERVER_DEPLOY_CMD:-}" "Scope=server_only: set repo var SERVER_DEPLOY_CMD." ;; full_stack) run_or_echo "${FULL_STACK_DEPLOY_CMD:-}" "Scope=full_stack: set repo var FULL_STACK_DEPLOY_CMD." ;; infra_only) run_or_echo "${INFRA_APPLY_CMD:-}" "Scope=infra_only: set repo var INFRA_APPLY_CMD." ;; *) echo "Unknown scope: $SCOPE" exit 1 ;; esac - name: Persist preview slot state shell: bash run: | if [ ! -f "$PREVIEW_STATE_FILE" ]; then exit 0 fi if [ -z "$(git status --porcelain -- "$PREVIEW_STATE_FILE")" ]; then exit 0 fi git config user.name "gitea-actions" git config user.email "gitea-actions@local" git add "$PREVIEW_STATE_FILE" git commit -m "chore: update preview slot state [skip ci]" || true git push || true - name: Summary shell: bash run: | echo "branch: ${{ steps.target.outputs.branch }}" echo "issue: ${{ steps.target.outputs.issue }}" echo "scope: ${{ steps.scope.outputs.scope }}" echo "slot: ${{ steps.slot.outputs.slot }}" echo "url: ${{ steps.slot.outputs.url }}"