feat: create git tag automatically to trigger releases (#1951)

This PR introduces multiple GitHub Actions to automate the release
procedure. In brief, it contains:

- **For nightly releases**: A fully automated GitHub Action that updates
dependencies (including dependencies of typstyle, typst.ts, and
typst-ansi-hl), releases nightly RC (aka canary version in the action
script) and nightly builds, along with its helper script (which can also
be useful for manually updating versions).

- **For stable releases**: Two GitHub Actions, one that detects newly
opened PRs containing tagging directives (`+Tag vx.y.z-rcw`) and leaves
comments, and another that detects merged tagging PRs and performs the
actual tagging.


Examples:
- Nightly release:
4708018995
- Stable release: ParaN3xus/tinymist#1, ParaN3xus/tinymist#2

Extra work needed to merge this PR:
- [ ] Remove all `nightly/*` branches and create `nightly` branch
- [ ] Add `NIGHTLY_REPO_TOKEN` secret to this repo

---------

Co-authored-by: Myriad-Dreamin <camiyoru@gmail.com>
This commit is contained in:
ParaN3xus 2025-07-31 20:59:11 +08:00 committed by GitHub
parent 9f7c21bb0c
commit 3aa9c9def0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 1161 additions and 1 deletions

149
.github/workflows/auto-tag.yml vendored Normal file
View file

@ -0,0 +1,149 @@
name: tinymist::auto_tag
on:
push:
branches:
- main
jobs:
auto-tag:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Get merged PR info
id: get-pr
run: |
COMMIT_SHA="${{ github.sha }}"
PR_NUMBER=$(gh pr list --state merged --limit 50 --json number,mergeCommit \
--jq ".[] | select(.mergeCommit.oid == \"$COMMIT_SHA\") | .number")
if [ -n "$PR_NUMBER" ]; then
echo "pr_number=$PR_NUMBER" >> $GITHUB_OUTPUT
echo "Found merged PR: #$PR_NUMBER"
else
echo "pr_number=" >> $GITHUB_OUTPUT
echo "No merged PR found for this commit"
fi
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Check for tag directive in merged PR
if: steps.get-pr.outputs.pr_number != ''
id: check-tag
uses: actions/github-script@v7
with:
script: |
const prNumber = '${{ steps.get-pr.outputs.pr_number }}';
if (!prNumber) {
console.log('No PR number found');
core.setOutput('tag_found', 'false');
return;
}
try {
const { data: pr } = await github.rest.pulls.get({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: parseInt(prNumber)
});
const prBody = pr.body || '';
console.log('PR Body:', prBody);
const tagRegex = /\+tag\s+(v\d+\.\d+\.\d+(?:-[a-zA-Z0-9]+)?)/;
const match = prBody.match(tagRegex);
if (match) {
const tagVersion = match[1];
console.log('Found tag directive:', tagVersion);
core.setOutput('tag_found', 'true');
core.setOutput('tag_version', tagVersion);
} else {
console.log('No tag directive found in merged PR');
core.setOutput('tag_found', 'false');
}
} catch (error) {
console.error('Error fetching PR:', error);
core.setOutput('tag_found', 'false');
}
- name: Check if tag already exists
if: steps.check-tag.outputs.tag_found == 'true'
id: check-existing-tag
run: |
TAG="${{ steps.check-tag.outputs.tag_version }}"
if git tag -l | grep -q "^$TAG$"; then
echo "tag_exists=true" >> $GITHUB_OUTPUT
echo "Tag $TAG already exists"
else
echo "tag_exists=false" >> $GITHUB_OUTPUT
echo "Tag $TAG does not exist, safe to create"
fi
- name: Create tag
if: steps.check-tag.outputs.tag_found == 'true' && steps.check-existing-tag.outputs.tag_exists == 'false'
run: |
TAG="${{ steps.check-tag.outputs.tag_version }}"
PR_NUMBER="${{ steps.get-pr.outputs.pr_number }}"
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git tag -a "$TAG" -m "Auto-created tag $TAG from PR #$PR_NUMBER"
git push origin "$TAG"
echo "Created and pushed tag: $TAG"
- name: Comment on merged PR
if: steps.check-tag.outputs.tag_found == 'true' && steps.check-existing-tag.outputs.tag_exists == 'false'
uses: actions/github-script@v6
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const tagVersion = '${{ steps.check-tag.outputs.tag_version }}';
const prNumber = '${{ steps.get-pr.outputs.pr_number }}';
const comment = `**Tag Created Successfully**
Tag \`${tagVersion}\` has been automatically created and pushed to the repository following the merge of this PR.
You can view the tag here: https://github.com/${{ github.repository }}/releases/tag/${tagVersion}`;
github.rest.issues.createComment({
issue_number: parseInt(prNumber),
owner: context.repo.owner,
repo: context.repo.repo,
body: comment
});
- name: Handle tag creation error
if: steps.check-tag.outputs.tag_found == 'true' && steps.check-existing-tag.outputs.tag_exists == 'true'
uses: actions/github-script@v6
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const tagVersion = '${{ steps.check-tag.outputs.tag_version }}';
const prNumber = '${{ steps.get-pr.outputs.pr_number }}';
const actionUrl = `${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}`;
const comment = `**Tag Creation Failed**
Could not create tag \`${tagVersion}\`.
Please refer to [this action run](${actionUrl}) for more information.`;
github.rest.issues.createComment({
issue_number: parseInt(prNumber),
owner: context.repo.owner,
repo: context.repo.repo,
body: comment
});

62
.github/workflows/detect-pr-tag.yml vendored Normal file
View file

@ -0,0 +1,62 @@
name: tinymist::detect_pr_tag
on:
pull_request:
types: [opened, edited]
branches:
- main
jobs:
detect-tag:
runs-on: ubuntu-latest
steps:
- name: Check tag in PR body
id: check-tag
uses: actions/github-script@v7
with:
script: |
const prBody = context.payload.pull_request.body || '';
console.log('PR Body:', prBody);
const tagRegex = /\+tag\s+(v\d+\.\d+\.\d+(?:-[a-zA-Z0-9]+)?)/;
const match = prBody.match(tagRegex);
if (match) {
const tagVersion = match[1];
console.log('Found tag:', tagVersion);
core.setOutput('tag_found', 'true');
core.setOutput('tag_version', tagVersion);
} else {
console.log('No tag found in PR description');
core.setOutput('tag_found', 'false');
}
- name: Comment on PR
if: steps.check-tag.outputs.tag_found == 'true'
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const tagVersion = '${{ steps.check-tag.outputs.tag_version }}';
const comment = `**Tag Detection Notice**
This PR contains a tag directive: \`+tag ${tagVersion}\`
If this PR is merged, it will automatically create tag \`${tagVersion}\` on the main branch.
Please ensure before merging:
- [ ] **Cargo.toml & Cargo.lock**: No \`git\` dependencies with \`branch\`, use \`tag\` or \`rev\` dependencies instead
- [ ] **Publish tokens**: Both \`VSCODE_MARKETPLACE_TOKEN\` and \`OPENVSX_ACCESS_TOKEN\` are valid and not expired
- [ ] **Version updates**: All version numbers in \`Cargo.toml\`, \`package.json\` and other files have been updated consistently
- [ ] **Changelog**: \`editors/vscode/CHANGELOG.md\` has been updated with correct format
- [ ] **tinymist-assets**: If needed, the crate has been published and version updated in \`Cargo.toml\`
`
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: comment
});

343
.github/workflows/release-nightly.yml vendored Normal file
View file

@ -0,0 +1,343 @@
name: Nightly Release
on:
schedule:
- cron: '0 0 * * *'
- cron: '0 23 * * *'
workflow_dispatch:
inputs:
release_type:
description: 'Release type'
required: true
default: 'nightly'
type: choice
options:
- nightly
- canary
jobs:
check-and-release:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
ref: nightly
token: ${{ secrets.NIGHTLY_REPO_TOKEN }}
fetch-depth: 0
- name: Install Node.js
uses: actions/setup-node@v4
with:
node-version: 22
cache: 'yarn'
- name: Install deps
run: yarn install
- name: Setup Git
run: |
git config --global user.name "GitHub Actions"
git config --global user.email "actions@github.com"
- name: Install system dependencies
run: |
sudo apt-get update
sudo apt-get install -y jq --no-install-recommends
- name: Determine release type
id: release_type
run: |
if [[ "${{ github.event_name }}" == "schedule" ]]; then
if [[ "${{ github.event.schedule }}" == "0 0 * * *" ]]; then
echo "release_type=nightly" >> $GITHUB_ENV
else
echo "release_type=canary" >> $GITHUB_ENV
fi
else
echo "release_type=${{ github.event.inputs.release_type }}" >> $GITHUB_ENV
fi
- name: Check for updates
id: check_updates
run: |
echo "Checking for updates in dependency repositories..."
# Get current revs using script
eval "$(node $GITHUB_WORKSPACE/scripts/nightly-utils.mjs . get-current-revs)"
# Get latest revs
latest_typst_rev=$(curl -s "https://api.github.com/repos/ParaN3xus/typst/commits/nightly-content-hint" | jq -r '.sha')
latest_reflexo_rev=$(curl -s "https://api.github.com/repos/ParaN3xus/typst.ts/commits/nightly" | jq -r '.sha')
latest_typstyle_rev=$(curl -s "https://api.github.com/repos/ParaN3xus/typstyle/commits/nightly" | jq -r '.sha')
latest_typst_ansi_hl_rev=$(curl -s "https://api.github.com/repos/ParaN3xus/typst-ansi-hl/commits/nightly" | jq -r '.sha')
echo "Current revs: typst=$current_typst_rev, typst.ts=$current_reflexo_rev, typstyle=$current_typstyle_rev, hl=$current_typst_ansi_hl_rev"
echo "Latest revs: typst=$latest_typst_rev, typst.ts=$latest_reflexo_rev, typstyle=$latest_typstyle_rev, hl=$latest_typst_ansi_hl_rev"
# Check for updates
need_update=false
if [[ "$current_typst_rev" != "$latest_typst_rev" ]] || [[ -z "$current_typst_rev" ]]; then
echo "Typst needs update"
need_update=true
fi
if [[ "$current_reflexo_rev" != "$latest_reflexo_rev" ]] || [[ -z "$current_reflexo_rev" ]]; then
echo "Typst.ts needs update"
need_update=true
fi
if [[ "$current_typstyle_rev" != "$latest_typstyle_rev" ]] || [[ -z "$current_typstyle_rev" ]]; then
echo "Typstyle needs update"
need_update=true
fi
if [[ "$current_typst_ansi_hl_rev" != "$latest_typst_ansi_hl_rev" ]] || [[ -z "$current_typst_ansi_hl_rev" ]]; then
echo "Typst-ansi-hl needs update"
need_update=true
fi
current_version=$(grep '^version = ' Cargo.toml | head -1 | cut -d'"' -f2)
echo "current_version=$current_version" >> $GITHUB_ENV
# Updates can only be performed when releasing an RC.
# When an RC has been released and there are no subsequent updates,
# this indicates that the RC release was successful. Only at this
# point will the nightly release be published.
need_release=false
if [ "$release_type" = "nightly" ]; then
if [ "$need_update" = "false" ] && echo "$current_version" | grep -q -- '-rc[0-9]\+$'; then
echo "RC version detected with no updates needed, nightly release condition met"
need_release=true
else
echo "Nightly release condition not met (requires stable RC version)"
fi
elif [ "$release_type" = "canary" ]; then
if [ "$need_update" = "true" ]; then
echo "Code updates detected, canary release condition met"
need_release=true
else
echo "No code updates, skipping canary release"
fi
fi
echo "Final decision: need_release=$need_release"
echo "need_release=$need_release" >> $GITHUB_OUTPUT
echo "latest_typst_rev=$latest_typst_rev" >> $GITHUB_ENV
- name: Calculate new version
id: version
if: steps.check_updates.outputs.need_release == 'true'
run: |
echo "Current version: $current_version"
echo "Release type: $release_type"
new_version=$(node $GITHUB_WORKSPACE/scripts/nightly-utils.mjs . calculate-version "$current_version" "$release_type")
echo "New version: $new_version"
echo "new_version=$new_version" >> $GITHUB_ENV
- uses: actions-rust-lang/setup-rust-toolchain@v1
- name: Get typst information
id: typst_info
if: steps.check_updates.outputs.need_release == 'true'
run: |
# Clone typst repository
git clone --depth 50 --single-branch --branch nightly-content-hint \
--filter=blob:limit=1k https://github.com/ParaN3xus/typst.git /tmp/typst
cd /tmp/typst
git checkout nightly-content-hint
# Get version
typst_version=$(grep '^version = ' Cargo.toml | head -1 | cut -d'"' -f2)
typst_assets_rev=$(grep 'typst-assets.*git' Cargo.toml | grep 'rev = ' | cut -d'"' -f4)
echo "typst_version=$typst_version" >> $GITHUB_ENV
echo "typst_assets_rev=$typst_assets_rev" >> $GITHUB_ENV
# Get base commit
git remote add upstream https://github.com/typst/typst.git && git fetch upstream main --prune
typst_base_commit=$(git merge-base HEAD upstream/main 2>/dev/null)
typst_base_msg=$(git --no-pager log --format="%s" -1 $base_sha)
echo "typst_base_commit=$typst_base_commit" >> $GITHUB_ENV
echo "typst_base_msg=$typst_base_msg" >> $GITHUB_ENV
- name: Update typst dependencies in tinymist
if: steps.check_updates.outputs.need_release == 'true'
run: |
node $GITHUB_WORKSPACE/scripts/nightly-utils.mjs . update-typst-deps \
"$typst_version" \
"$typst_assets_rev"
revs_json=$(cat <<EOF
{
"typst": "${latest_typst_rev}"
}
EOF
)
node $GITHUB_WORKSPACE/scripts/nightly-utils.mjs . update-patch-revs "$revs_json"
- name: Update world crates version
if: steps.check_updates.outputs.need_release == 'true'
run: |
node $GITHUB_WORKSPACE/scripts/nightly-utils.mjs . bump-world-crates "$new_version"
cargo update -p tinymist-derive -p tinymist-l10n -p tinymist-package -p tinymist-std -p tinymist-vfs -p tinymist-world -p tinymist-project -p tinymist-task -p typst-shim
git add -A
git commit -m "build: bump world crates to $new_version"
git push origin nightly
world_commit=$(git rev-parse HEAD)
echo "world_commit=$world_commit" >> $GITHUB_ENV
- name: Update typst.ts
if: steps.check_updates.outputs.need_release == 'true'
run: |
# Clone typst.ts
git clone https://${{ secrets.NIGHTLY_REPO_TOKEN }}@github.com/ParaN3xus/typst.ts.git /tmp/typst.ts
cd /tmp/typst.ts
git checkout nightly
new_version="$new_version"
node $GITHUB_WORKSPACE/scripts/nightly-utils.mjs . update-world-crates "$new_version"
# Update typst dependencies
node $GITHUB_WORKSPACE/scripts/nightly-utils.mjs . update-typst-deps \
"$typst_version" \
"$typst_assets_rev"
# Update patches
revs_json=$(cat <<EOF
{
"tinymist": "${world_commit}",
"typst": "${latest_typst_rev}"
}
EOF
)
node $GITHUB_WORKSPACE/scripts/nightly-utils.mjs . update-patch-revs "$revs_json"
cargo update
git add -A
git commit -m "build: update tinymist and typst"
git push origin nightly
reflexo_commit=$(git rev-parse HEAD)
echo "reflexo_commit=$reflexo_commit" >> $GITHUB_ENV
- name: Update typstyle
if: steps.check_updates.outputs.need_release == 'true'
run: |
# Clone typstyle
git clone https://${{ secrets.NIGHTLY_REPO_TOKEN }}@github.com/ParaN3xus/typstyle.git /tmp/typstyle
cd /tmp/typstyle
git checkout nightly
node $GITHUB_WORKSPACE/scripts/nightly-utils.mjs . update-world-crates "$new_version"
node $GITHUB_WORKSPACE/scripts/nightly-utils.mjs . update-typst-deps \
"$typst_version" \
"$typst_assets_rev"
# Update patches
revs_json=$(cat <<EOF
{
"tinymist": "${world_commit}",
"typst": "${latest_typst_rev}"
}
EOF
)
node $GITHUB_WORKSPACE/scripts/nightly-utils.mjs . update-patch-revs "$revs_json"
cargo update
git add -A
git commit -m "build: update tinymist to ${new_version}"
git push origin nightly
typstyle_commit=$(git rev-parse HEAD)
echo "typstyle_commit=$typstyle_commit" >> $GITHUB_ENV
- name: Update typst-ansi-hl
if: steps.check_updates.outputs.need_release == 'true'
run: |
# Clone typst-ansi-hl
git clone https://${{ secrets.NIGHTLY_REPO_TOKEN }}@github.com/ParaN3xus/typst-ansi-hl.git /tmp/typst-ansi-hl
cd /tmp/typst-ansi-hl
git checkout nightly
node $GITHUB_WORKSPACE/scripts/nightly-utils.mjs . update-typst-deps \
"$typst_version" \
"$typst_assets_rev"
# Update patches
revs_json=$(cat <<EOF
{
"typst": "${latest_typst_rev}"
}
EOF
)
node $GITHUB_WORKSPACE/scripts/nightly-utils.mjs . update-patch-revs "$revs_json"
cargo update
git add -A
git commit -m "build: update typst-syntax" || true
git push origin nightly
hl_commit=$(git rev-parse HEAD)
echo "hl_commit=$hl_commit" >> $GITHUB_ENV
- name: Update tinymist patches and versions
if: steps.check_updates.outputs.need_release == 'true'
run: |
# Update patch revisions using script
revs_json=$(cat <<EOF
{
"reflexo": "${reflexo_commit}",
"typst-ansi-hl": "${hl_commit}",
"typstyle": "${typstyle_commit}"
}
EOF
)
node $GITHUB_WORKSPACE/scripts/nightly-utils.mjs . update-patch-revs "$revs_json"
# Update main version
node $GITHUB_WORKSPACE/scripts/nightly-utils.mjs . update-main-version "$new_version"
- name: Update version files
if: steps.check_updates.outputs.need_release == 'true'
run: |
node $GITHUB_WORKSPACE/scripts/nightly-utils.mjs . update-version-files "$new_version"
- name: Generate changelog
if: steps.check_updates.outputs.need_release == 'true'
run: |
tinymist_base_commit=$(git merge-base HEAD origin/main)
tinymist_base_msg=$(git --no-pager log --format="%s" -1 $base_sha)
node $GITHUB_WORKSPACE/scripts/nightly-utils.mjs . generate-changelog \
"$new_version" \
"$tinymist_base_commit" \
"$tinymist_base_msg" \
"$latest_typst_rev" \
"$typst_base_commit" \
"$typst_base_msg"
- name: Final commit and tag
if: steps.check_updates.outputs.need_release == 'true'
run: |
new_version="$new_version"
cargo update
git add -A
git commit -m "build: bump version to ${new_version}"
git tag "v${new_version}"
git push origin nightly
git push origin "v${new_version}"
echo "Successfully released tinymist ${new_version}!"
- name: No updates needed
if: steps.check_updates.outputs.need_release != 'true'
run: |
echo "No updates needed. All dependencies are up to date."