CI/CD #455
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # Agent rules for generation: | |
| # https://arran4.com/post/2026/006-Github-CI-and-Deploy/ | |
| name: CI/CD | |
| on: | |
| push: | |
| branches: [main, master] | |
| tags: | |
| - 'v*' | |
| - 'v*.*.*' | |
| - 'v*.*.*-rc*' | |
| - 'v*.*.*-beta*' | |
| - 'test-*' | |
| pull_request: | |
| types: [opened, synchronize, reopened, ready_for_review, closed] | |
| branches: [main, master] | |
| release: | |
| types: [published] | |
| workflow_dispatch: | |
| inputs: | |
| mode: | |
| description: "Pipeline mode" | |
| required: true | |
| default: "lint-fix" | |
| type: choice | |
| options: | |
| - lint-fix | |
| - build | |
| - release-major | |
| - release-minor | |
| - release-patch | |
| - release-test | |
| - release-rc | |
| - release-alpha | |
| - monthly-maintenance | |
| release_version_override: | |
| description: "Optional explicit release version (for example 2.4.0 or 2.4.0-rc.2)" | |
| required: false | |
| default: "" | |
| type: string | |
| allow_prs: | |
| description: "Allow automation to open pull requests" | |
| required: false | |
| default: true | |
| type: boolean | |
| schedule: | |
| - cron: '17 19 1 * *' | |
| - cron: '41 19 * * *' | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.event.pull_request.number || github.ref }} | |
| cancel-in-progress: true | |
| permissions: | |
| contents: write | |
| pull-requests: write | |
| checks: write | |
| packages: write | |
| security-events: write | |
| jobs: | |
| route: | |
| name: Route event | |
| runs-on: ubuntu-latest | |
| outputs: | |
| run_code_checks: ${{ steps.route.outputs.run_code_checks }} | |
| run_pr_meta_checks: ${{ steps.route.outputs.run_pr_meta_checks }} | |
| run_cleanup: ${{ steps.route.outputs.run_cleanup }} | |
| run_release: ${{ steps.route.outputs.run_release }} | |
| is_monthly: ${{ steps.route.outputs.is_monthly }} | |
| is_nightly: ${{ steps.route.outputs.is_nightly }} | |
| steps: | |
| - id: route | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| run_code_checks=false | |
| run_pr_meta_checks=false | |
| run_cleanup=false | |
| run_release=false | |
| is_monthly=false | |
| is_nightly=false | |
| case "${{ github.event_name }}" in | |
| push) | |
| run_code_checks=true | |
| if [[ "${{ github.ref }}" == refs/tags/v* ]]; then | |
| run_release=true | |
| fi | |
| ;; | |
| pull_request) | |
| if [[ "${{ github.event.action }}" == "closed" ]]; then | |
| run_cleanup=true | |
| else | |
| run_pr_meta_checks=true | |
| if [[ "${{ github.event.pull_request.head.repo.full_name || '' }}" != "${{ github.repository }}" ]]; then | |
| run_code_checks=true | |
| fi | |
| fi | |
| ;; | |
| release) | |
| run_release=true | |
| ;; | |
| workflow_dispatch) | |
| run_code_checks=true | |
| if [[ "${{ inputs.mode }}" == release-* ]]; then | |
| run_release=true | |
| fi | |
| if [[ "${{ inputs.mode }}" == "monthly-maintenance" ]]; then | |
| is_monthly=true | |
| fi | |
| if [[ "${{ inputs.mode }}" == "lint-fix" ]]; then | |
| is_nightly=true | |
| fi | |
| ;; | |
| schedule) | |
| run_code_checks=true | |
| if [[ "${{ github.event.schedule }}" == "17 19 1 * *" ]]; then | |
| is_monthly=true | |
| fi | |
| if [[ "${{ github.event.schedule }}" == "41 19 * * *" ]]; then | |
| is_nightly=true | |
| fi | |
| ;; | |
| esac | |
| echo "run_code_checks=$run_code_checks" >> "$GITHUB_OUTPUT" | |
| echo "run_pr_meta_checks=$run_pr_meta_checks" >> "$GITHUB_OUTPUT" | |
| echo "run_cleanup=$run_cleanup" >> "$GITHUB_OUTPUT" | |
| echo "run_release=$run_release" >> "$GITHUB_OUTPUT" | |
| echo "is_monthly=$is_monthly" >> "$GITHUB_OUTPUT" | |
| echo "is_nightly=$is_nightly" >> "$GITHUB_OUTPUT" | |
| discover: | |
| name: Discover capabilities and cost profile | |
| needs: route | |
| runs-on: ubuntu-latest | |
| outputs: | |
| profile: ${{ steps.profile.outputs.profile }} | |
| has_docker: ${{ steps.detect.outputs.has_docker }} | |
| has_packaging: ${{ steps.detect.outputs.has_packaging }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - id: detect | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| echo "has_docker=true" >> "$GITHUB_OUTPUT" | |
| echo "has_packaging=true" >> "$GITHUB_OUTPUT" | |
| - id: profile | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| if [[ "${{ github.event.repository.private }}" == "true" ]]; then | |
| echo "profile=private" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "profile=public" >> "$GITHUB_OUTPUT" | |
| fi | |
| gitleaks: | |
| name: Secret scan | |
| needs: [route, discover] | |
| if: ${{ needs.route.outputs.run_code_checks == 'true' }} | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - uses: gitleaks/gitleaks-action@v2 | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| shell-check: | |
| name: ShellCheck and config check | |
| needs: [route, discover] | |
| if: ${{ needs.route.outputs.run_code_checks == 'true' }} | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Install ShellCheck and zsh | |
| run: sudo apt-get update && sudo apt-get install -y shellcheck zsh | |
| - run: | | |
| shopt -s globstar | |
| shellcheck **/*.sh | |
| - run: | | |
| yes "" | sh -c "$(curl -fsLS get.chezmoi.io)" -- init --no-tty --debug --source=$PWD --apply | |
| - name: Verify shell configuration parses | |
| run: | | |
| for f in ~/.bashrc ~/.bash_profile ~/.bash_login ~/.bash_logout ~/.profile; do | |
| if [ -f "$f" ]; then | |
| bash -n "$f" | |
| fi | |
| done | |
| for f in ~/.zshrc ~/.zprofile ~/.zlogin ~/.zlogout ~/.zshenv; do | |
| if [ -f "$f" ]; then | |
| zsh -n "$f" | |
| fi | |
| done | |
| docker-build: | |
| name: Build dev-dotfiles-debian image | |
| needs: [route, discover] | |
| if: ${{ needs.discover.outputs.has_docker == 'true' && needs.route.outputs.run_release == 'true' }} | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: docker/setup-qemu-action@v3 | |
| - uses: docker/setup-buildx-action@v3 | |
| - uses: docker/build-push-action@v6 | |
| with: | |
| context: . | |
| build-contexts: dotfiles=. | |
| file: containers/dev-dotfiles-debian/Dockerfile | |
| push: false | |
| tags: ghcr.io/${{ github.repository_owner }}/dev-dotfiles-debian:ci-${{ github.run_id }} | |
| docker-release: | |
| name: Docker release | |
| needs: [route, discover, docker-build] | |
| if: ${{ needs.discover.outputs.has_docker == 'true' && needs.route.outputs.run_release == 'true' }} | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| packages: write | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: docker/setup-qemu-action@v3 | |
| - uses: docker/setup-buildx-action@v3 | |
| - uses: docker/login-action@v3 | |
| with: | |
| registry: ghcr.io | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Extract Docker metadata | |
| id: meta | |
| uses: docker/metadata-action@v5 | |
| with: | |
| images: ghcr.io/${{ github.repository_owner }}/dev-dotfiles-debian | |
| labels: | | |
| org.opencontainers.image.description=Debian development container with dotfiles | |
| tags: | | |
| type=ref,event=tag | |
| type=semver,pattern=v{{version}} | |
| type=semver,pattern=v{{major}}.{{minor}} | |
| type=semver,pattern=v{{major}} | |
| type=semver,pattern=v{{version}},value=${{ inputs.release_version_override }},enable=${{ inputs.release_version_override != '' }} | |
| type=semver,pattern=v{{major}}.{{minor}},value=${{ inputs.release_version_override }},enable=${{ inputs.release_version_override != '' }} | |
| type=semver,pattern=v{{major}},value=${{ inputs.release_version_override }},enable=${{ inputs.release_version_override != '' }} | |
| type=raw,value=${{ inputs.release_version_override }},enable=${{ inputs.release_version_override != '' }} | |
| type=raw,value=latest,enable={{is_default_branch}} | |
| - uses: docker/build-push-action@v6 | |
| with: | |
| context: . | |
| build-contexts: dotfiles=. | |
| file: containers/dev-dotfiles-debian/Dockerfile | |
| push: true | |
| tags: ${{ steps.meta.outputs.tags }} | |
| labels: ${{ steps.meta.outputs.labels }} | |
| annotations: ${{ steps.meta.outputs.annotations }} | |
| package-dotfiles: | |
| name: Package Dotfiles | |
| needs: [route, discover] | |
| if: ${{ needs.discover.outputs.has_packaging == 'true' && needs.route.outputs.run_release == 'true' }} | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - name: Apply configuration | |
| run: | | |
| yes "" | sh -c "$(curl -fsLS get.chezmoi.io)" -- init --no-tty --debug --source=$PWD --apply | |
| - name: Create archive | |
| run: | | |
| ./bin/chezmoi archive --source=$PWD --format zip --output dotfiles.zip | |
| - uses: actions/upload-artifact@v4 | |
| with: | |
| name: dotfiles-archive | |
| path: dotfiles.zip | |
| retention-days: 1 | |
| publish-draft: | |
| name: Publish draft release assets | |
| needs: [package-dotfiles, docker-release] | |
| if: ${{ needs.route.outputs.run_release == 'true' }} | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Collect artifacts | |
| uses: actions/download-artifact@v4 | |
| with: | |
| path: dist-release | |
| pattern: dotfiles-* | |
| - name: Publish draft GitHub release | |
| uses: softprops/action-gh-release@v2 | |
| with: | |
| draft: true | |
| files: dist-release/dotfiles-archive/dotfiles.zip | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| promote-release: | |
| name: Promote draft to published | |
| needs: [publish-draft] | |
| if: ${{ github.event_name == 'release' || (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')) || (github.event_name == 'workflow_dispatch' && startsWith(inputs.mode, 'release-')) }} | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Promote release | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| if [[ "${{ github.event_name }}" == "push" && "${{ github.ref }}" == refs/tags/v* ]]; then | |
| gh release edit "${{ github.ref_name }}" --draft=false | |
| else | |
| echo "Promotion step placeholder (gh api patch release draft=false)" | |
| fi |