Skip to content

Release

Release #51

Workflow file for this run

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