Release #394
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: | |
| branches: [main] | |
| workflow_dispatch: | |
| inputs: | |
| runner: | |
| description: 'Runner to use (manual runs only)' | |
| type: choice | |
| options: | |
| - arc-runner | |
| - ubuntu-latest | |
| default: arc-runner | |
| concurrency: | |
| group: release-${{ github.ref_name }} | |
| cancel-in-progress: false | |
| permissions: | |
| contents: write | |
| packages: write | |
| id-token: write # Required for cosign keyless signing | |
| env: | |
| REGISTRY: ghcr.io | |
| BACKEND_IMAGE: ghcr.io/${{ github.repository }}-api | |
| WEB_IMAGE: ghcr.io/${{ github.repository }}-web | |
| NOTIFICATION_IMAGE: ghcr.io/${{ github.repository }}-notification-worker | |
| EXPORT_IMAGE: ghcr.io/${{ github.repository }}-export-worker | |
| AUTOMATION_IMAGE: ghcr.io/${{ github.repository }}-automation-worker | |
| jobs: | |
| version-tag: | |
| name: Version & Tag | |
| runs-on: ${{ inputs.runner || 'arc-runner' }} | |
| outputs: | |
| new_tag: ${{ steps.bump.outputs.new_tag }} | |
| version: ${{ steps.bump.outputs.version }} | |
| is_prerelease: ${{ steps.bump.outputs.is_prerelease }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - name: Determine version bump | |
| id: bump | |
| run: | | |
| LATEST=$(git tag --sort=-v:refname | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | head -1) | |
| if [ -z "$LATEST" ]; then | |
| LATEST="v0.0.0" | |
| fi | |
| echo "Latest tag: $LATEST" | |
| MAJOR=$(echo "$LATEST" | sed 's/v//' | cut -d. -f1) | |
| MINOR=$(echo "$LATEST" | sed 's/v//' | cut -d. -f2) | |
| PATCH=$(echo "$LATEST" | sed 's/v//' | cut -d. -f3) | |
| if [ "$LATEST" = "v0.0.0" ]; then | |
| COMMITS=$(git log --oneline) | |
| else | |
| COMMITS=$(git log "${LATEST}..HEAD" --oneline) | |
| fi | |
| if echo "$COMMITS" | grep -qiE '^[a-f0-9]+ (feat!|BREAKING)'; then | |
| MAJOR=$((MAJOR + 1)); MINOR=0; PATCH=0 | |
| elif echo "$COMMITS" | grep -qiE '^[a-f0-9]+ feat'; then | |
| MINOR=$((MINOR + 1)); PATCH=0 | |
| else | |
| PATCH=$((PATCH + 1)) | |
| fi | |
| BASE_VERSION="${MAJOR}.${MINOR}.${PATCH}" | |
| # Non-main branches get a prerelease suffix | |
| if [ "${{ github.ref_name }}" != "main" ]; then | |
| BRANCH=$(echo "${{ github.ref_name }}" | sed 's/[^a-zA-Z0-9-]/-/g' | sed 's/--*/-/g' | sed 's/^-//;s/-$//') | |
| SHORT_SHA=$(git rev-parse --short HEAD) | |
| VERSION="${BASE_VERSION}-rc.${BRANCH}.g${SHORT_SHA}" | |
| NEW_TAG="v${VERSION}" | |
| IS_PRERELEASE="true" | |
| else | |
| VERSION="${BASE_VERSION}" | |
| NEW_TAG="v${VERSION}" | |
| IS_PRERELEASE="false" | |
| fi | |
| echo "New tag: $NEW_TAG" | |
| echo "new_tag=$NEW_TAG" >> "$GITHUB_OUTPUT" | |
| echo "version=$VERSION" >> "$GITHUB_OUTPUT" | |
| echo "is_prerelease=$IS_PRERELEASE" >> "$GITHUB_OUTPUT" | |
| - name: Create and push tag | |
| run: | | |
| git config user.name "github-actions[bot]" | |
| git config user.email "github-actions[bot]@users.noreply.github.com" | |
| git tag -a "${{ steps.bump.outputs.new_tag }}" -m "Release ${{ steps.bump.outputs.new_tag }}" | |
| git push origin "${{ steps.bump.outputs.new_tag }}" | |
| docker: | |
| name: Docker Β· ${{ matrix.image }} | |
| needs: version-tag | |
| runs-on: ${{ inputs.runner || 'arc-runner' }} | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - image: api | |
| file: Dockerfile | |
| build-args: | | |
| VERSION=${{ needs.version-tag.outputs.new_tag }} | |
| COMMIT=${{ github.sha }} | |
| - image: web | |
| file: Dockerfile.web | |
| build-args: "" | |
| - image: notification-worker | |
| file: Dockerfile.notification | |
| build-args: | | |
| VERSION=${{ needs.version-tag.outputs.new_tag }} | |
| - image: export-worker | |
| file: Dockerfile.export-worker | |
| build-args: | | |
| VERSION=${{ needs.version-tag.outputs.new_tag }} | |
| - image: automation-worker | |
| file: Dockerfile.automation | |
| build-args: | | |
| VERSION=${{ needs.version-tag.outputs.new_tag }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: docker/setup-buildx-action@v3 | |
| - name: Log in to GHCR | |
| uses: docker/login-action@v3 | |
| with: | |
| registry: ghcr.io | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Determine image tags | |
| id: tags | |
| run: | | |
| BASE="ghcr.io/${{ github.repository }}-${{ matrix.image }}" | |
| VERSION="${{ needs.version-tag.outputs.version }}" | |
| if [ "${{ needs.version-tag.outputs.is_prerelease }}" = "true" ]; then | |
| echo "tags=${BASE}:${VERSION}" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "tags=${BASE}:${VERSION},${BASE}:latest" >> "$GITHUB_OUTPUT" | |
| fi | |
| - name: Build and push ${{ matrix.image }} | |
| uses: docker/build-push-action@v5 | |
| id: docker-build | |
| with: | |
| context: . | |
| file: ${{ matrix.file }} | |
| push: true | |
| tags: ${{ steps.tags.outputs.tags }} | |
| cache-from: type=gha,scope=${{ matrix.image }} | |
| cache-to: type=gha,mode=max,scope=${{ matrix.image }} | |
| build-args: ${{ matrix.build-args }} | |
| - name: Install cosign | |
| uses: sigstore/cosign-installer@v3 | |
| - name: Sign image with cosign (keyless) | |
| env: | |
| DIGEST: ${{ steps.docker-build.outputs.digest }} | |
| TAGS: ${{ steps.tags.outputs.tags }} | |
| run: | | |
| IMAGE=$(echo "$TAGS" | cut -d',' -f1) | |
| cosign sign --yes "${IMAGE}@${DIGEST}" | |
| echo "β Signed ${IMAGE}@${DIGEST}" | |
| # Phase-53 / p53-sbom β Software Bill of Materials. | |
| # syft generates a CycloneDX SBOM of the image contents (every | |
| # Go module, OS package, and JS dependency that landed in the | |
| # final layer). The SBOM is attested to the image via cosign so | |
| # consumers can `cosign verify-attestation --type cyclonedx` | |
| # without trusting any side-channel artifact server. | |
| - name: Install syft | |
| uses: anchore/sbom-action/download-syft@v0.17.8 | |
| - name: Generate SBOM (CycloneDX) | |
| env: | |
| DIGEST: ${{ steps.docker-build.outputs.digest }} | |
| TAGS: ${{ steps.tags.outputs.tags }} | |
| run: | | |
| IMAGE=$(echo "$TAGS" | cut -d',' -f1) | |
| syft "${IMAGE}@${DIGEST}" \ | |
| -o cyclonedx-json="sbom-${{ matrix.image }}.cdx.json" \ | |
| -o spdx-json="sbom-${{ matrix.image }}.spdx.json" | |
| echo "β Generated SBOMs for ${IMAGE}" | |
| ls -la sbom-${{ matrix.image }}.*.json | |
| - name: Attest SBOM with cosign (keyless) | |
| env: | |
| DIGEST: ${{ steps.docker-build.outputs.digest }} | |
| TAGS: ${{ steps.tags.outputs.tags }} | |
| run: | | |
| IMAGE=$(echo "$TAGS" | cut -d',' -f1) | |
| cosign attest --yes \ | |
| --predicate "sbom-${{ matrix.image }}.cdx.json" \ | |
| --type cyclonedx \ | |
| "${IMAGE}@${DIGEST}" | |
| echo "β Attested CycloneDX SBOM to ${IMAGE}@${DIGEST}" | |
| - name: Upload SBOM artifacts | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: sbom-${{ matrix.image }}-${{ needs.version-tag.outputs.version }} | |
| path: | | |
| sbom-${{ matrix.image }}.cdx.json | |
| sbom-${{ matrix.image }}.spdx.json | |
| retention-days: 90 | |
| helm: | |
| name: Publish Helm Chart | |
| needs: [version-tag, docker] | |
| runs-on: ${{ inputs.runner || 'arc-runner' }} | |
| permissions: | |
| contents: read | |
| packages: write | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Install Helm | |
| uses: azure/setup-helm@v4 | |
| with: | |
| version: v3.16.0 | |
| - name: Log in to GHCR | |
| run: echo "${{ secrets.GITHUB_TOKEN }}" | helm registry login ghcr.io -u ${{ github.actor }} --password-stdin | |
| - name: Package and push chart | |
| run: | | |
| helm package helm/teslasync \ | |
| --version ${{ needs.version-tag.outputs.version }} \ | |
| --app-version ${{ needs.version-tag.outputs.version }} | |
| helm push teslasync-*.tgz oci://ghcr.io/${{ github.repository_owner }}/charts | |
| release-notes: | |
| name: Create GitHub Release | |
| needs: [version-tag, docker, helm] | |
| if: ${{ !cancelled() && needs.docker.result == 'success' && needs.helm.result == 'success' }} | |
| runs-on: ${{ inputs.runner || 'arc-runner' }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - name: Generate release notes | |
| run: | | |
| TAG="${{ needs.version-tag.outputs.new_tag }}" | |
| VERSION="${{ needs.version-tag.outputs.version }}" | |
| IS_PRE="${{ needs.version-tag.outputs.is_prerelease }}" | |
| PREV=$(git tag --sort=-v:refname | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | grep -v "^${TAG}$" | head -1) | |
| if [ "$IS_PRE" = "true" ]; then | |
| LABEL="β οΈ Pre-release (branch: ${{ github.ref_name }})" | |
| else | |
| LABEL="π TeslaSync ${TAG}" | |
| fi | |
| if [ -n "$PREV" ]; then | |
| CHANGELOG=$(git log "${PREV}..HEAD" --pretty=format:"- %s (@%an)" --no-merges) | |
| COMPARE="**Full Changelog**: https://github.com/${{ github.repository }}/compare/${PREV}...${TAG}" | |
| else | |
| CHANGELOG=$(git log --pretty=format:"- %s (@%an)" --no-merges -20) | |
| COMPARE="" | |
| fi | |
| cat > release-notes.md << NOTES_EOF | |
| ## ${LABEL} | |
| ### π¦ Docker Images | |
| \`\`\`bash | |
| # Backend API | |
| docker pull ghcr.io/${{ github.repository }}-api:${VERSION} | |
| # Web Frontend | |
| docker pull ghcr.io/${{ github.repository }}-web:${VERSION} | |
| # Notification Worker | |
| docker pull ghcr.io/${{ github.repository }}-notification-worker:${VERSION} | |
| # Export Worker | |
| docker pull ghcr.io/${{ github.repository }}-export-worker:${VERSION} | |
| # Automation Worker | |
| docker pull ghcr.io/${{ github.repository }}-automation-worker:${VERSION} | |
| \`\`\` | |
| ### β Helm Chart | |
| \`\`\`bash | |
| helm install teslasync oci://ghcr.io/${{ github.repository_owner }}/charts/teslasync --version ${VERSION} | |
| \`\`\` | |
| ### π Image Verification | |
| All images are signed with [cosign](https://github.com/sigstore/cosign) (keyless, via GitHub OIDC) and ship with a CycloneDX SBOM attestation: | |
| \`\`\`bash | |
| # Verify image signature | |
| cosign verify ghcr.io/${{ github.repository }}-api:${VERSION} \\ | |
| --certificate-identity-regexp="github.com/${{ github.repository }}" \\ | |
| --certificate-oidc-issuer="https://token.actions.githubusercontent.com" | |
| # Verify + extract SBOM attestation | |
| cosign verify-attestation \\ | |
| --type cyclonedx \\ | |
| --certificate-identity-regexp="github.com/${{ github.repository }}" \\ | |
| --certificate-oidc-issuer="https://token.actions.githubusercontent.com" \\ | |
| ghcr.io/${{ github.repository }}-api:${VERSION} \\ | |
| | jq -r '.payload' | base64 -d | jq '.predicate' > sbom.cdx.json | |
| \`\`\` | |
| ### π Changes | |
| ${CHANGELOG} | |
| ${COMPARE} | |
| ### π Assets | |
| | Asset | Tag | | |
| |-------|-----| | |
| | Backend Image | \`ghcr.io/${{ github.repository }}-api:${VERSION}\` | | |
| | Web Image | \`ghcr.io/${{ github.repository }}-web:${VERSION}\` | | |
| | Notification Worker | \`ghcr.io/${{ github.repository }}-notification-worker:${VERSION}\` | | |
| | Export Worker | \`ghcr.io/${{ github.repository }}-export-worker:${VERSION}\` | | |
| | Automation Worker | \`ghcr.io/${{ github.repository }}-automation-worker:${VERSION}\` | | |
| | Helm Chart | \`oci://ghcr.io/${{ github.repository_owner }}/charts/teslasync:${VERSION}\` | | |
| NOTES_EOF | |
| - name: Create GitHub Release | |
| uses: softprops/action-gh-release@v2 | |
| with: | |
| tag_name: ${{ needs.version-tag.outputs.new_tag }} | |
| name: TeslaSync ${{ needs.version-tag.outputs.new_tag }} | |
| body_path: release-notes.md | |
| prerelease: ${{ needs.version-tag.outputs.is_prerelease == 'true' }} | |
| generate_release_notes: false |