diff --git a/.github/workflows/auto-tag.yml b/.github/workflows/auto-tag.yml new file mode 100644 index 00000000..10675efe --- /dev/null +++ b/.github/workflows/auto-tag.yml @@ -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 + }); diff --git a/.github/workflows/detect-pr-tag.yml b/.github/workflows/detect-pr-tag.yml new file mode 100644 index 00000000..eb45bebf --- /dev/null +++ b/.github/workflows/detect-pr-tag.yml @@ -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 + }); diff --git a/.github/workflows/release-nightly.yml b/.github/workflows/release-nightly.yml new file mode 100644 index 00000000..77af6443 --- /dev/null +++ b/.github/workflows/release-nightly.yml @@ -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 <> $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 <> $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 <> $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 <> $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 < { + if (typeof depInfo === 'string') return null; + if (typeof depInfo === 'object' && depInfo.rev) return depInfo.rev; + return null; + }; + + return { + typst: extractRev(patches.typst), + reflexo: extractRev(patches.reflexo), + typstyle: extractRev(patches['typstyle-core']), + 'typst-ansi-hl': extractRev(patches['typst-ansi-hl']) + }; + } + + async updateDependencies(crates, version) { + await this.ensureInit(); + const cargoTomlPath = path.join(this.rootDir, 'Cargo.toml'); + const { content } = await this.readToml(cargoTomlPath); + + let updatedContent = content; + + const updateCrateDependencyVersion = (content, crate, newVersion) => { + const parsed = parse(content) + const deps = parsed?.workspace?.dependencies || {}; + + if (!(crate in deps)) { + throw Error("Missing package") + } + + const crateDepInfo = deps[crate]; + + if (typeof crateDepInfo === 'string') { + return edit(content, `workspace.dependencies.${crate}`, newVersion) + } + if (typeof crateDepInfo === 'object' && crateDepInfo.version) { + return edit(content, `workspace.dependencies.${crate}.version`, newVersion) + } + + throw Error("Invalid dependency info") + } + + for (const crate of crates) { + try { + updatedContent = updateCrateDependencyVersion(updatedContent, crate, version) + } catch (e) { + } + } + + await this.writeToml(cargoTomlPath, updatedContent); + } + + async updateTypstDependencies(typstVersion, typstAssetsRev) { + await this.ensureInit(); + + const typstCrates = [ + 'typst-cli', + 'typst-eval', + 'typst-html', + 'typst-ide', + 'typst-kit', + 'typst-layout', + 'typst-library', + 'typst-macros', + 'typst-pdf', + 'typst-realize', + 'typst-render', + 'typst-svg', + 'typst-syntax', + 'typst-timing', + 'typst-utils', + 'typst', + ]; + await this.updateDependencies(typstCrates, typstVersion) + + const cargoTomlPath = path.join(this.rootDir, 'Cargo.toml'); + const { content } = await this.readToml(cargoTomlPath); + + let updatedContent = content; + + try { + updatedContent = edit(updatedContent, 'workspace.dependencies.typst-assets.rev', typstAssetsRev); + } catch (e) { + console.warn(`Warning: Could not update typst - assets rev: ${e.message} `); + } + + await this.writeToml(cargoTomlPath, updatedContent); + } + + async bumpWorldCrates(newVersion, bump = false) { + await this.ensureInit(); + + const worldCrates = [ + 'tinymist-derive', 'tinymist-l10n', 'tinymist-package', 'tinymist-std', + 'tinymist-vfs', 'tinymist-world', 'tinymist-project', 'tinymist-task', 'typst-shim' + ]; + await this.updateDependencies(worldCrates, newVersion); + + if (!bump) { + return + } + + for (const crate of worldCrates) { + const cratePath = path.join(this.rootDir, 'crates', crate, 'Cargo.toml'); + try { + const { content: crateContent } = await this.readToml(cratePath); + const updatedCrateContent = edit(crateContent, 'package.version', newVersion); + await this.writeToml(cratePath, updatedCrateContent); + } catch (e) { + console.warn(`Warning: Could not update ${crate}/Cargo.toml: ${e.message}`); + } + } + } + + async updatePatchRevs(revs) { + await this.ensureInit(); + const cargoTomlPath = path.join(this.rootDir, 'Cargo.toml'); + const { content, parsed } = await this.readToml(cargoTomlPath); + + let updatedContent = content; + + const patchMappings = [ + { key: 'reflexo', patches: ['reflexo', 'reflexo-typst', 'reflexo-vec2svg'] }, + { key: 'typst-ansi-hl', patches: ['typst-ansi-hl'] }, + { key: 'typstyle', patches: ['typstyle-core'] }, + { + key: 'typst', patches: [ + 'typst-cli', + 'typst-eval', + 'typst-html', + 'typst-ide', + 'typst-kit', + 'typst-layout', + 'typst-library', + 'typst-macros', + 'typst-pdf', + 'typst-realize', + 'typst-render', + 'typst-svg', + 'typst-syntax', + 'typst-timing', + 'typst-utils', + 'typst', + ] + }, + { + key: 'tinymist', patches: [ + 'crityp', + 'tinymist', + 'tinymist-assets', + 'tinymist-dap', + 'tinymist-derive', + 'tinymist-lint', + 'tinymist-project', + 'tinymist-render', + 'tinymist-task', + 'tinymist-vfs', + 'typlite', + 'typst-shim', + 'sync-lsp', + 'tinymist-analysis', + 'tinymist-core', + 'tinymist-debug', + 'tinymist-l10n', + 'tinymist-package', + 'tinymist-query', + 'tinymist-std', + 'tinymist-tests', + 'tinymist-world', + 'typst-preview', + ] + }, + ]; + + for (const mapping of patchMappings) { + if (revs[mapping.key]) { + for (const patchName of mapping.patches) { + try { + let patchInfo = parsed?.patch?.['crates-io'][patchName] || null + if (!patchInfo) { + continue; + } + + delete patchInfo.branch; + delete patchInfo.tag; + + patchInfo.rev = revs[mapping.key]; + updatedContent = edit(updatedContent, `patch.crates-io.${patchName}`, patchInfo); + } catch (e) { + console.warn(`Warning: Could not update ${patchName} rev: ${e.message}`); + } + } + } + } + + await this.writeToml(cargoTomlPath, updatedContent); + } + + async updateMainVersion(newVersion) { + await this.ensureInit(); + + const nonWorldCrates = [ + 'sync-ls', 'tinymist', 'tinymist-analysis', 'tinymist-core', 'tinymist-debug', + 'tinymist-lint', 'tinymist-query', 'tinymist-render', 'tinymist-preview', 'typlite' + ]; + await this.updateDependencies(nonWorldCrates, newVersion); + + const cargoTomlPath = path.join(this.rootDir, 'Cargo.toml'); + const { content } = await this.readToml(cargoTomlPath); + + let updatedContent = edit(content, 'workspace.package.version', newVersion); + + await this.writeToml(cargoTomlPath, updatedContent); + } + + async updateVersionFiles(newVersion) { + const jsonFiles = [ + 'contrib/html/editors/vscode/package.json', + 'crates/tinymist-core/package.json', + 'editors/vscode/package.json', + 'syntaxes/textmate/package.json' + ]; + + for (const file of jsonFiles) { + const filePath = path.join(this.rootDir, file); + try { + const json = await this.readJson(filePath); + json.version = newVersion; + await this.writeJson(filePath, json); + } catch (e) { + console.warn(`Warning: Could not update ${file}: ${e.message}`); + } + } + + await this.updateSpecialFiles(newVersion); + } + + async updateSpecialFiles(newVersion) { + // Nix flake + try { + const nixFlakePath = path.join(this.rootDir, 'contrib/nix/dev/flake.nix'); + let nixContent = await fs.readFile(nixFlakePath, 'utf-8'); + nixContent = nixContent.replace( + /version = "[^"]*";/g, + `version = "${newVersion}";` + ); + await fs.writeFile(nixFlakePath, nixContent); + } catch (e) { + console.warn(`Warning: Could not update flake.nix: ${e.message}`); + } + + // Dockerfile + try { + const dockerfilePath = path.join(this.rootDir, 'editors/neovim/samples/lazyvim-dev/Dockerfile'); + let dockerContent = await fs.readFile(dockerfilePath, 'utf-8'); + dockerContent = dockerContent.replace( + /FROM myriaddreamin\/tinymist:[^ ]* as tinymist/g, + `FROM myriaddreamin/tinymist:${newVersion} as tinymist` + ); + await fs.writeFile(dockerfilePath, dockerContent); + } catch (e) { + console.warn(`Warning: Could not update Dockerfile: ${e.message}`); + } + + // bootstrap.sh + try { + const bootstrapPath = path.join(this.rootDir, 'editors/neovim/bootstrap.sh'); + let bootstrapContent = await fs.readFile(bootstrapPath, 'utf-8'); + bootstrapContent = bootstrapContent.replace( + /myriaddreamin\/tinymist:[^ ]*/g, + `myriaddreamin/tinymist:${newVersion}` + ); + bootstrapContent = bootstrapContent.replace( + /myriaddreamin\/tinymist-nvim:[^ ]*/g, + `myriaddreamin/tinymist-nvim:${newVersion}` + ); + await fs.writeFile(bootstrapPath, bootstrapContent); + } catch (e) { + console.warn(`Warning: Could not update bootstrap.sh: ${e.message}`); + } + } + + async generateChangelog(newVersion, tinymistBaseCommit, tinymistBaseMessage, typstRev, typstBaseCommit, typstBaseMessage) { + const currentDate = new Date().toISOString().split('T')[0]; + const changelogPath = path.join(this.rootDir, 'editors/vscode/CHANGELOG.md'); + + // Template for the new changelog entry. + const newChangelogEntryTemplate = `## v${newVersion} - [${currentDate}] + +Nightly Release at [${tinymistBaseMessage}](https://github.com/Myriad-Dreamin/tinymist/commit/${tinymistBaseCommit}), using [ParaN3xus/typst rev ${typstRev.slice(0, 7)}](https://github.com/ParaN3xus/typst/commit/${typstRev}), a.k.a. [typst/typst ${typstBaseMessage}](https://github.com/typst/typst/commit/${typstBaseCommit}). + +**Full Changelog**: https://github.com/Myriad-Dreamin/tinymist/compare/{{PREV_VERSION}}...v${newVersion} +`; + + try { + const content = await fs.readFile(changelogPath, 'utf-8'); + const lines = content.split('\n'); + + const newVersionBase = `v${newVersion.split('-')[0]}`; + + const filteredLines = []; + let isSkipping = false; + + // skip lines with same base ver + for (const line of lines) { + if (line.startsWith('## v')) { + const match = line.match(/## (v[0-9]+\.[0-9]+\.[0-9]+)/); + if (match && match[1] === newVersionBase) { + isSkipping = true; + } else { + isSkipping = false; + } + } + + if (!isSkipping) { + filteredLines.push(line); + } + } + + // find insert point + let previousVersion = ''; + for (const line of filteredLines) { + if (line.startsWith('## v')) { + const match = line.match(/## (v[0-9][^\s]*)/); + if (match) { + previousVersion = match[1]; + break; + } + } + } + + let finalEntry = newChangelogEntryTemplate.replace( + '{{PREV_VERSION}}', + previousVersion || 'HEAD~1' + ); + + const firstReleaseIndex = filteredLines.findIndex(line => line.startsWith('## v')); + + let finalContent; + if (firstReleaseIndex >= 0) { + const newLines = [ + ...filteredLines.slice(0, firstReleaseIndex), + finalEntry, + ...filteredLines.slice(firstReleaseIndex) + ]; + finalContent = newLines.join('\n'); + } else { + // append + finalContent = filteredLines.join('\n').trim() + '\n\n' + finalEntry; + } + + await fs.writeFile(changelogPath, finalContent.trim() + '\n'); + + } catch (e) { + // create + console.warn(`Warning: Could not update CHANGELOG.md: ${e.message}. Creating a new one.`); + const finalEntry = newChangelogEntryTemplate.replace('{{PREV_VERSION}}', 'HEAD~1'); + await fs.writeFile(changelogPath, finalEntry); + } + } + + calculateNewVersion(currentVersion, releaseType) { + const validVersionRegex = /^[0-9.\-rc]+$/; + if (!validVersionRegex.test(currentVersion)) { + throw new Error(`Invalid version format: ${currentVersion}. Version can only contain numbers, dots, hyphens, and 'rc'.`); + } + + if (releaseType === 'canary') { + const [baseVersion, suffix] = currentVersion.split('-'); + const versionParts = baseVersion.split('.'); + const currentPatch = parseInt(versionParts[2]); + + if (currentPatch % 2 === 0) { + // even -> +1-rc1 + const newPatch = currentPatch + 1; + const newVersion = `${versionParts[0]}.${versionParts[1]}.${newPatch}`; + return `${newVersion}-rc1`; + } else { + // odd + if (suffix && suffix.startsWith('rc')) { + const rcNumber = parseInt(suffix.replace('rc', '')); + + if (rcNumber === 9) { + // rc9 -> +1-rc1 + const newPatch = currentPatch + 2; + const newVersion = `${versionParts[0]}.${versionParts[1]}.${newPatch}`; + return `${newVersion}-rc1`; + } else { + // rc -> rc+1 + const newRcNumber = rcNumber + 1; + return `${baseVersion}-rc${newRcNumber}`; + } + } else { + // no rc -> +2-rc1 + const newPatch = currentPatch + 2; + const newVersion = `${versionParts[0]}.${versionParts[1]}.${newPatch}`; + return `${newVersion}-rc1`; + } + } + } else { + // nightly release + // simply remove -rc + const baseVersion = currentVersion.split('-')[0]; + const versionParts = baseVersion.split('.'); + const currentPatch = parseInt(versionParts[2]); + + if (currentPatch % 2 === 0) { + throw new Error(`Current patch version ${currentPatch} is not odd. Nightly releases require odd patch versions.`); + } + + return baseVersion; + } + } + +} + +async function main() { + const rootDir = process.argv[2]; + + const utils = new NightlyUtils(rootDir); + + const command = process.argv[3]; + + if (!command) { + console.error('Please specify a command'); + process.exit(1); + } + + try { + switch (command) { + case 'get-current-revs': { + const revs = await utils.getCurrentDependencyRevs(); + Object.entries(revs).forEach(([key, value]) => { + console.log(`current_${key.replaceAll('-', '_')}_rev=${value || ''}`); + }); + break; + } + + case 'update-typst-deps': { + const typstVersion = process.argv[4]; + const assetsRev = process.argv[5]; + if (!typstVersion || !assetsRev) { + throw new Error('Usage: update-typst-deps '); + } + await utils.updateTypstDependencies(typstVersion, assetsRev); + console.log(`Updated typst dependencies to ${typstVersion}`); + break; + } + + case 'update-world-crates': { + const newVersion = process.argv[4]; + if (!newVersion) { + throw new Error('Usage: update-world-crates '); + } + await utils.bumpWorldCrates(newVersion, false); + console.log(`Updated world crates to ${newVersion}`); + break; + } + + case 'bump-world-crates': { + const newVersion = process.argv[4]; + if (!newVersion) { + throw new Error('Usage: bump-world-crates '); + } + await utils.bumpWorldCrates(newVersion, true); + console.log(`Updated world crates to ${newVersion}`); + break; + } + + case 'update-patch-revs': { + const revsJson = process.argv[4]; + if (!revsJson) { + throw new Error('Usage: update-patch-revs '); + } + const revs = JSON.parse(revsJson); + await utils.updatePatchRevs(revs); + console.log('Updated patch revisions'); + break; + } + + case 'update-main-version': { + const newVersion = process.argv[4]; + if (!newVersion) { + throw new Error('Usage: update-main-version '); + } + await utils.updateMainVersion(newVersion); + console.log(`Updated main version to ${newVersion}`); + break; + } + + case 'update-version-files': { + const newVersion = process.argv[4]; + if (!newVersion) { + throw new Error('Usage: update-version-files '); + } + await utils.updateVersionFiles(newVersion); + console.log(`Updated version files to ${newVersion}`); + break; + } + + case 'generate-changelog': { + const newVersion = process.argv[4]; + const tinymistBaseCommit = process.argv[5]; + const tinymistBaseMessage = process.argv[6]; + const typstRev = process.argv[7]; + const typstBaseCommit = process.argv[8]; + const typstBaseMessage = process.argv[9]; + if (!newVersion || !tinymistBaseCommit || !tinymistBaseMessage || !typstRev || !typstBaseCommit || !typstBaseMessage) { + throw new Error('Usage: generate-changelog '); + } + await utils.generateChangelog(newVersion, tinymistBaseCommit, tinymistBaseMessage, typstRev, typstBaseCommit, typstBaseMessage); + console.log('Generated changelog'); + break; + } + + case 'calculate-version': { + const currentVersion = process.argv[4]; + const releaseType = process.argv[5]; + if (!currentVersion || !releaseType) { + throw new Error('Usage: calculate-version '); + } + const newVersion = utils.calculateNewVersion(currentVersion, releaseType); + console.log(newVersion); + break; + } + + default: + console.error(`Unknown command: ${command}`); + console.error('Available commands:'); + console.error(' get-current-revs'); + console.error(' update-typst-deps '); + console.error(' bump-world-crates '); + console.error(' update-world-crates '); + console.error(' update-patch-revs '); + console.error(' update-main-version '); + console.error(' update-version-files '); + console.error(' generate-changelog '); + console.error(' calculate-version '); + process.exit(1); + } + } catch (error) { + console.error('Error:', error.message); + process.exit(1); + } +} + +if (import.meta.url === `file://${process.argv[1]}`) { + main(); +} + +export default NightlyUtils; diff --git a/yarn.lock b/yarn.lock index 70623b9f..02d74d79 100644 --- a/yarn.lock +++ b/yarn.lock @@ -449,6 +449,11 @@ resolved "https://registry.yarnpkg.com/@protobufjs/utf8/-/utf8-1.1.0.tgz#a777360b5b39a1a2e5106f8e858f2fd2d060c570" integrity sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw== +"@rainbowatcher/toml-edit-js@^0.5.1": + version "0.5.1" + resolved "https://registry.yarnpkg.com/@rainbowatcher/toml-edit-js/-/toml-edit-js-0.5.1.tgz#1c205eede4f9ac7b955402b755126ebe60c83509" + integrity sha512-9Q7CGm24nvJyDy4STQvrPrA09U7zgLJ7GaLHBiJhA8vVoRjmfsCG9R0PjJtzytU4FUD2FiZvBlN5KCTOux/gFQ== + "@rollup/rollup-android-arm-eabi@4.34.8": version "4.34.8" resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.34.8.tgz#731df27dfdb77189547bcef96ada7bf166bbb2fb"