Harden self-hosted release workflow (#1) #3
Workflow file for this run
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
| name: Release | |
| on: | |
| push: | |
| tags: | |
| - "v0.*.*" | |
| permissions: | |
| contents: write | |
| packages: write | |
| concurrency: | |
| group: release-${{ github.ref }} | |
| cancel-in-progress: false | |
| jobs: | |
| validate: | |
| name: Release-ready validation | |
| uses: ./.github/workflows/release-validation.yml | |
| publish: | |
| name: Publish container and release notes | |
| runs-on: ubuntu-latest | |
| needs: validate | |
| steps: | |
| - name: Checkout release tag | |
| uses: actions/checkout@v7 | |
| with: | |
| fetch-depth: 0 | |
| - name: Verify release tag belongs to master | |
| run: | | |
| git fetch origin master:refs/remotes/origin/master | |
| git merge-base --is-ancestor "$GITHUB_SHA" origin/master | |
| - name: Log in to GitHub Container Registry | |
| env: | |
| REGISTRY: ghcr.io | |
| USERNAME: ${{ github.actor }} | |
| PASSWORD: ${{ secrets.GITHUB_TOKEN }} | |
| run: echo "$PASSWORD" | docker login "$REGISTRY" --username "$USERNAME" --password-stdin | |
| - name: Set up Docker Buildx | |
| uses: docker/setup-buildx-action@v3 | |
| - name: Determine image tags | |
| id: versions | |
| run: echo "minor=${GITHUB_REF_NAME%.*}" >> "$GITHUB_OUTPUT" | |
| - name: Publish application image | |
| id: image | |
| uses: docker/build-push-action@v6 | |
| with: | |
| context: . | |
| push: true | |
| tags: | | |
| ghcr.io/${{ github.repository }}:${{ github.ref_name }} | |
| ghcr.io/${{ github.repository }}:${{ steps.versions.outputs.minor }} | |
| ghcr.io/${{ github.repository }}:latest | |
| labels: | | |
| org.opencontainers.image.source=${{ github.server_url }}/${{ github.repository }} | |
| org.opencontainers.image.revision=${{ github.sha }} | |
| org.opencontainers.image.version=${{ github.ref_name }} | |
| cache-from: type=gha,scope=release | |
| cache-to: type=gha,mode=max,scope=release | |
| provenance: mode=max | |
| sbom: true | |
| - name: Smoke test published image | |
| env: | |
| IMAGE: ghcr.io/${{ github.repository }} | |
| DIGEST: ${{ steps.image.outputs.digest }} | |
| run: | | |
| docker pull "$IMAGE@$DIGEST" | |
| docker run --detach --rm --name versiontracker --publish 8080:80 \ | |
| --env APP_ENV=production \ | |
| --env APP_DEBUG=false \ | |
| --env APP_KEY=base64:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= \ | |
| "$IMAGE@$DIGEST" | |
| for attempt in {1..30}; do | |
| if curl --fail --silent http://127.0.0.1:8080/up; then | |
| exit 0 | |
| fi | |
| sleep 2 | |
| done | |
| docker logs versiontracker | |
| exit 1 | |
| - name: Scan published image | |
| uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 | |
| with: | |
| image-ref: ghcr.io/${{ github.repository }}@${{ steps.image.outputs.digest }} | |
| format: table | |
| exit-code: "1" | |
| ignore-unfixed: true | |
| severity: CRITICAL,HIGH | |
| vuln-type: os,library | |
| - name: Validate published installer path | |
| env: | |
| VERSION: ${{ github.ref_name }} | |
| COMPOSE_PROJECT_NAME: versiontracker-release-install | |
| run: | | |
| test -x install.sh | |
| printf '%s\n' 'Release Test Password 2026' | ./install.sh install \ | |
| --version "$VERSION" \ | |
| --mode proxy \ | |
| --port 18080 \ | |
| --admin-name 'Release Test' \ | |
| --admin-email release-test@example.invalid \ | |
| --admin-password-stdin | |
| ./install.sh status | |
| backup_directory="$(./install.sh backup)" | |
| test -s "$backup_directory/database.sql" | |
| test -s "$backup_directory/storage.tar.gz" | |
| - name: Build deployment bundle | |
| env: | |
| VERSION: ${{ github.ref_name }} | |
| run: | | |
| bundle="versiontracker-deploy-${VERSION}.tar.gz" | |
| tar --create --gzip --file "$bundle" \ | |
| --transform "s,^,versiontracker-deploy-${VERSION}/," \ | |
| install.sh compose.yml compose.proxy.yml compose.caddy.yml Caddyfile .env.docker.example docs/self-hosting.md | |
| sha256sum "$bundle" > "${bundle}.sha256" | |
| - name: Clean up release installation test | |
| if: always() | |
| env: | |
| COMPOSE_PROJECT_NAME: versiontracker-release-install | |
| run: docker compose --env-file .env.docker -f compose.yml -f compose.proxy.yml down --volumes --remove-orphans | |
| - name: Create GitHub release | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| IMAGE: ghcr.io/${{ github.repository }} | |
| DIGEST: ${{ steps.image.outputs.digest }} | |
| run: | | |
| gh release create "$GITHUB_REF_NAME" \ | |
| --generate-notes \ | |
| --title "$GITHUB_REF_NAME" \ | |
| --notes $'### Container image\n\n`'"$IMAGE@$DIGEST"$'\n\n### Deployment bundle\n\nDownload the `versiontracker-deploy-` archive and verify its SHA-256 checksum before installation.\n\n### Upgrade notes\n\nSee the self-hosting guide for backup, update, and rollback instructions.' \ | |
| "versiontracker-deploy-${GITHUB_REF_NAME}.tar.gz" \ | |
| "versiontracker-deploy-${GITHUB_REF_NAME}.tar.gz.sha256" |