Release #51
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: | |
| workflow_dispatch: | |
| env: | |
| REGISTRY: ghcr.io | |
| IMAGE_NAME: ${{ github.repository }} | |
| jobs: | |
| release: | |
| runs-on: ubuntu-latest | |
| if: github.ref == 'refs/heads/main' | |
| concurrency: | |
| group: ghcr-${{ github.repository }} | |
| cancel-in-progress: false | |
| permissions: | |
| contents: write | |
| packages: write | |
| pull-requests: read | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v5 | |
| with: | |
| fetch-depth: 0 | |
| - name: Set up Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: 24.18.0 | |
| - name: Set up pnpm | |
| uses: pnpm/action-setup@v4 | |
| - name: Install dependencies | |
| run: pnpm install --frozen-lockfile | |
| - name: Verify project | |
| run: pnpm run verify | |
| - name: Calculate version | |
| id: version | |
| run: | | |
| YEAR=$(date -u +%Y) | |
| MONTH=$(date -u +%-m) | |
| PREFIX="${YEAR}.${MONTH}." | |
| git fetch --tags | |
| PREVIOUS_TAG=$(git tag -l "[0-9]*.[0-9]*.[0-9]*" --sort=-v:refname | head -1) | |
| LATEST_N=$(git tag -l "${PREFIX}*" --sort=-v:refname | head -1 | sed "s/^${PREFIX}//") | |
| if [ -z "$LATEST_N" ]; then | |
| NEXT_N=1 | |
| else | |
| NEXT_N=$((LATEST_N + 1)) | |
| fi | |
| VERSION="${YEAR}.${MONTH}.${NEXT_N}" | |
| echo "version=${VERSION}" >> $GITHUB_OUTPUT | |
| echo "previous_tag=${PREVIOUS_TAG}" >> $GITHUB_OUTPUT | |
| echo "date=$(date -u +'%Y-%m-%d')" >> $GITHUB_OUTPUT | |
| echo "sha8=$(echo ${GITHUB_SHA} | cut -c1-8)" >> $GITHUB_OUTPUT | |
| echo "Calculated version: ${VERSION}" | |
| - name: Set up Docker Buildx | |
| uses: docker/setup-buildx-action@v3 | |
| - name: Log in to the Container registry | |
| uses: docker/login-action@v3 | |
| with: | |
| registry: ${{ env.REGISTRY }} | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Extract metadata for Docker | |
| id: meta | |
| uses: docker/metadata-action@v5 | |
| with: | |
| images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} | |
| tags: | | |
| type=raw,value=latest | |
| type=raw,value=${{ steps.version.outputs.version }} | |
| - name: Build and push Docker image | |
| uses: docker/build-push-action@v5 | |
| with: | |
| context: . | |
| push: true | |
| platforms: linux/amd64,linux/arm64 | |
| tags: ${{ steps.meta.outputs.tags }} | |
| labels: ${{ steps.meta.outputs.labels }} | |
| build-args: | | |
| VITE_COMMIT_HASH=${{ steps.version.outputs.sha8 }} | |
| VITE_BUILD_DATE=${{ steps.version.outputs.date }} | |
| VITE_REPO_URL=${{ github.server_url }}/${{ github.repository }} | |
| VITE_BRANCH=main | |
| VITE_VERSION=${{ steps.version.outputs.version }} | |
| DOCKER_IMAGE_REF=${{ github.repository }} | |
| - name: Create git tag | |
| run: | | |
| git config user.name "github-actions[bot]" | |
| git config user.email "github-actions[bot]@users.noreply.github.com" | |
| git tag -a "${{ steps.version.outputs.version }}" -m "Release ${{ steps.version.outputs.version }}" | |
| git push origin "${{ steps.version.outputs.version }}" | |
| - name: Build release notes | |
| uses: actions/github-script@v7 | |
| env: | |
| VERSION: ${{ steps.version.outputs.version }} | |
| PREVIOUS_TAG: ${{ steps.version.outputs.previous_tag }} | |
| REPOSITORY: ${{ github.repository }} | |
| with: | |
| script: | | |
| const fs = require("fs"); | |
| const { execFileSync } = require("child_process"); | |
| const version = process.env.VERSION; | |
| const previousTag = process.env.PREVIOUS_TAG; | |
| const repository = process.env.REPOSITORY; | |
| const [owner, repo] = repository.split("/"); | |
| const git = (args) => | |
| execFileSync("git", args, { encoding: "utf8" }).trim(); | |
| const prUrl = (number) => | |
| `https://github.com/${repository}/pull/${number}`; | |
| const commitUrl = (sha) => | |
| `https://github.com/${repository}/commit/${sha}`; | |
| const contributors = new Map(); | |
| const addContributor = (user) => { | |
| if (!user?.login || user.type === "Bot") return; | |
| contributors.set( | |
| user.login, | |
| user.html_url || `https://github.com/${user.login}` | |
| ); | |
| }; | |
| const range = previousTag ? `${previousTag}..HEAD` : "HEAD"; | |
| const log = git(["log", "--reverse", "--format=%H%x1f%s", range]); | |
| const commits = log | |
| ? log.split("\n").map((line) => { | |
| const [sha, subject] = line.split("\x1f"); | |
| return { sha, subject }; | |
| }) | |
| : []; | |
| const prNumbers = []; | |
| const pullRequests = new Map(); | |
| const directCommits = []; | |
| for (const commit of commits) { | |
| const { data: pulls } = | |
| await github.rest.repos.listPullRequestsAssociatedWithCommit({ | |
| owner, | |
| repo, | |
| commit_sha: commit.sha, | |
| }); | |
| const pullRequest = pulls.find((pull) => pull.merged_at) || pulls[0]; | |
| if (!pullRequest) { | |
| directCommits.push(commit); | |
| continue; | |
| } | |
| if (!pullRequests.has(pullRequest.number)) { | |
| prNumbers.push(pullRequest.number); | |
| pullRequests.set(pullRequest.number, pullRequest); | |
| addContributor(pullRequest.user); | |
| } | |
| } | |
| const lines = ["## Changes", ""]; | |
| if (prNumbers.length === 0 && directCommits.length === 0) { | |
| lines.push("- No code changes detected for this release.", ""); | |
| } | |
| for (const number of prNumbers) { | |
| const pullRequest = pullRequests.get(number); | |
| const prCommits = await github.paginate(github.rest.pulls.listCommits, { | |
| owner, | |
| repo, | |
| pull_number: number, | |
| per_page: 100, | |
| }); | |
| lines.push( | |
| `- ${pullRequest.title} ([#${number}](${prUrl(number)}))` | |
| ); | |
| for (const commit of prCommits) { | |
| addContributor(commit.author); | |
| } | |
| if (prCommits.length > 1) { | |
| for (const commit of prCommits) { | |
| const subject = commit.commit.message.split("\n")[0]; | |
| lines.push(` - ${subject}`); | |
| } | |
| } | |
| } | |
| if (directCommits.length > 0) { | |
| for (const commit of directCommits) { | |
| lines.push( | |
| `- ${commit.subject} ([${commit.sha.slice(0, 7)}](${commitUrl(commit.sha)}))` | |
| ); | |
| const { data: directCommit } = await github.rest.repos.getCommit({ | |
| owner, | |
| repo, | |
| ref: commit.sha, | |
| }); | |
| addContributor(directCommit.author); | |
| } | |
| } | |
| lines.push(""); | |
| if (contributors.size > 0) { | |
| lines.push("## Contributors", ""); | |
| for (const [login, url] of [...contributors].sort()) { | |
| lines.push(`- [@${login}](${url})`); | |
| } | |
| lines.push(""); | |
| } | |
| if (previousTag) { | |
| lines.push( | |
| `[Full changelog](https://github.com/${repository}/compare/${previousTag}...${version})`, | |
| "" | |
| ); | |
| } | |
| lines.push( | |
| "## Docker", | |
| "", | |
| "```bash", | |
| `docker pull ghcr.io/${repository}:${version}`, | |
| "```", | |
| "" | |
| ); | |
| fs.writeFileSync("release-notes.md", lines.join("\n")); | |
| - name: Create GitHub Release | |
| uses: softprops/action-gh-release@v2 | |
| with: | |
| tag_name: ${{ steps.version.outputs.version }} | |
| name: ${{ steps.version.outputs.version }} | |
| body_path: release-notes.md | |
| - name: Delete untagged images | |
| uses: dataaxiom/ghcr-cleanup-action@v1 | |
| with: | |
| token: ${{ secrets.GITHUB_TOKEN }} | |
| package: ${{ github.event.repository.name }} | |
| delete-untagged: true |