feat: simplify patch release workflow (#8196)

Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
Co-authored-by: Richie Foreman <richie.foreman@gmail.com>
This commit is contained in:
matt korwel
2025-09-12 10:22:10 -07:00
committed by GitHub
parent eaadc6d93d
commit c99539b991
10 changed files with 489 additions and 86 deletions
+60 -7
View File
@@ -19,9 +19,10 @@ inputs:
required: true required: true
dry-run: dry-run:
description: 'Whether to run in dry-run mode.' description: 'Whether to run in dry-run mode.'
type: 'boolean'
required: true required: true
release-branch: release-tag:
description: 'The branch to target for the release.' description: 'The release tag for the release (e.g., v0.1.11).'
required: true required: true
previous-tag: previous-tag:
description: 'The previous tag to use for generating release notes.' description: 'The previous tag to use for generating release notes.'
@@ -34,6 +35,44 @@ inputs:
runs: runs:
using: 'composite' using: 'composite'
steps: steps:
- name: 'Configure Git User'
working-directory: '${{ inputs.working-directory }}'
shell: 'bash'
run: |-
git config user.name "gemini-cli-robot"
git config user.email "gemini-cli-robot@google.com"
- name: 'Create and switch to a release branch'
working-directory: '${{ inputs.working-directory }}'
id: 'release_branch'
shell: 'bash'
run: |
BRANCH_NAME="release/${{ inputs.release-tag }}"
git switch -c "${BRANCH_NAME}"
echo "BRANCH_NAME=${BRANCH_NAME}" >> "${GITHUB_OUTPUT}"
- name: 'Update package versions'
working-directory: '${{ inputs.working-directory }}'
shell: 'bash'
run: 'npm run release:version "${{ inputs.release-version }}"'
- name: 'Commit and Conditionally Push package versions'
working-directory: '${{ inputs.working-directory }}'
shell: 'bash'
env:
BRANCH_NAME: '${{ steps.release_branch.outputs.BRANCH_NAME }}'
DRY_RUN: '${{ inputs.dry-run }}'
RELEASE_TAG: '${{ inputs.release-tag }}'
run: |-
git add package.json package-lock.json packages/*/package.json
git commit -m "chore(release): ${RELEASE_TAG}"
if [[ "${DRY_RUN}" == "false" ]]; then
echo "Pushing release branch to remote..."
git push --set-upstream origin "${BRANCH_NAME}" --follow-tags
else
echo "Dry run enabled. Skipping push."
fi
- name: 'Build and Prepare Packages' - name: 'Build and Prepare Packages'
working-directory: '${{ inputs.working-directory }}' working-directory: '${{ inputs.working-directory }}'
run: |- run: |-
@@ -61,7 +100,7 @@ runs:
- name: 'Install latest core package' - name: 'Install latest core package'
working-directory: '${{ inputs.working-directory }}' working-directory: '${{ inputs.working-directory }}'
if: '${{ inputs.dry-run == "false" }}' if: '${{ inputs.dry-run == false }}'
run: |- run: |-
npm install "@google/gemini-cli-core@${{ inputs.release-version }}" \ npm install "@google/gemini-cli-core@${{ inputs.release-version }}" \
--workspace="@google/gemini-cli" \ --workspace="@google/gemini-cli" \
@@ -86,14 +125,28 @@ runs:
- name: 'Create GitHub Release' - name: 'Create GitHub Release'
working-directory: '${{ inputs.working-directory }}' working-directory: '${{ inputs.working-directory }}'
if: '${{ inputs.dry-run == "false" }}' if: '${{ inputs.dry-run == false }}'
env: env:
GITHUB_TOKEN: '${{ inputs.github-token }}' GITHUB_TOKEN: '${{ inputs.github-token }}'
run: |- run: |-
gh release create "v${{ inputs.release-version }}" \ gh release create "${{ inputs.release-tag }}" \
bundle/gemini.js \ bundle/gemini.js \
--target "${{ inputs.release-branch }}" \ --target "${{ steps.release_branch.outputs.BRANCH_NAME }}" \
--title "Release v${{ inputs.release-version }}" \ --title "Release ${{ inputs.release-tag }}" \
--notes-start-tag "${{ inputs.previous-tag }}" \ --notes-start-tag "${{ inputs.previous-tag }}" \
--generate-notes --generate-notes
shell: 'bash' shell: 'bash'
- name: 'Create Issue on Failure'
if: |-
${{ failure() }}
env:
GITHUB_TOKEN: '${{ inputs.github-token }}'
RELEASE_TAG: '${{ inputs.release-tag }} || "N/A"'
DETAILS_URL: '${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}'
run: |-
gh issue create \
--title "Release Failed for ${RELEASE_TAG} on $(date +'%Y-%m-%d')" \
--body "The release workflow failed. See the full run for details: ${DETAILS_URL}" \
--label "kind/bug,release-failure,priority/p0"
shell: 'bash'
+24
View File
@@ -0,0 +1,24 @@
name: 'Run Tests'
description: 'Runs the preflight checks and integration tests.'
inputs:
force_skip_tests:
description: 'Whether to force skip the tests.'
required: false
default: 'false'
gemini_api_key:
description: 'The API key for running integration tests.'
required: true
runs:
using: 'composite'
steps:
- name: 'Run Tests'
if: "inputs.force_skip_tests != 'true'"
env:
GEMINI_API_KEY: '${{ inputs.gemini_api_key }}'
run: |-
npm run preflight
npm run test:integration:sandbox:none
npm run test:integration:sandbox:docker
shell: 'bash'
+12 -1
View File
@@ -10,6 +10,11 @@ on:
required: true required: true
type: 'boolean' type: 'boolean'
default: true default: true
force_skip_tests:
description: 'Select to skip the "Run Tests" step in testing. Prod releases should run tests'
required: false
type: 'boolean'
default: false
jobs: jobs:
release: release:
@@ -29,6 +34,12 @@ jobs:
- name: 'Install Dependencies' - name: 'Install Dependencies'
run: 'npm ci' run: 'npm ci'
- name: 'Run Tests'
uses: './.github/actions/run-tests'
with:
force_skip_tests: '${{ github.event.inputs.force_skip_tests }}'
gemini_api_key: '${{ secrets.GEMINI_API_KEY }}'
- name: 'Get Nightly Version' - name: 'Get Nightly Version'
id: 'nightly_version' id: 'nightly_version'
env: env:
@@ -44,10 +55,10 @@ jobs:
uses: './.github/actions/publish-release' uses: './.github/actions/publish-release'
with: with:
release-version: '${{ steps.nightly_version.outputs.RELEASE_VERSION }}' release-version: '${{ steps.nightly_version.outputs.RELEASE_VERSION }}'
release-tag: '${{ steps.nightly_version.outputs.RELEASE_TAG }}'
npm-tag: '${{ steps.nightly_version.outputs.NPM_TAG }}' npm-tag: '${{ steps.nightly_version.outputs.NPM_TAG }}'
wombat-token-core: '${{ secrets.WOMBAT_TOKEN_CORE }}' wombat-token-core: '${{ secrets.WOMBAT_TOKEN_CORE }}'
wombat-token-cli: '${{ secrets.WOMBAT_TOKEN_CLI }}' wombat-token-cli: '${{ secrets.WOMBAT_TOKEN_CLI }}'
github-token: '${{ secrets.GITHUB_TOKEN }}' github-token: '${{ secrets.GITHUB_TOKEN }}'
dry-run: '${{ github.event.inputs.dry_run }}' dry-run: '${{ github.event.inputs.dry_run }}'
release-branch: 'main'
previous-tag: '${{ steps.nightly_version.outputs.PREVIOUS_TAG }}' previous-tag: '${{ steps.nightly_version.outputs.PREVIOUS_TAG }}'
+94
View File
@@ -0,0 +1,94 @@
name: 'Patch Release'
on:
workflow_dispatch:
inputs:
type:
description: 'The type of release to patch from.'
required: true
type: 'choice'
options:
- 'stable'
- 'preview'
ref:
description: 'The branch or ref (full git sha) to release from.'
required: true
type: 'string'
default: 'main'
dry_run:
description: 'Run a dry-run of the release process; no branches, npm packages or GitHub releases will be created.'
required: true
type: 'boolean'
default: true
force_skip_tests:
description: 'Select to skip the "Run Tests" step in testing. Prod releases should run tests'
required: false
type: 'boolean'
default: false
jobs:
release:
runs-on: 'ubuntu-latest'
environment:
name: 'production-release'
url: '${{ github.server_url }}/${{ github.repository }}/releases/tag/${{ steps.version.outputs.RELEASE_TAG }}'
if: |-
${{ github.repository == 'google-gemini/gemini-cli' }}
permissions:
contents: 'write'
packages: 'write'
id-token: 'write'
issues: 'write' # For creating issues on failure
outputs:
RELEASE_TAG: '${{ steps.version.outputs.RELEASE_TAG }}'
steps:
- name: 'Checkout'
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5
with:
ref: '${{ github.event.inputs.ref || github.sha }}'
fetch-depth: 0
- name: 'Setup Node.js'
uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: 'npm'
- name: 'Install Dependencies'
run: |-
npm ci
- name: 'Get the version'
id: 'version'
env:
GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
run: |-
VERSION_JSON="$(node scripts/get-release-version.js --type=patch --patch-from=${{ github.event.inputs.type }})"
echo "${VERSION_JSON}"
echo "RELEASE_TAG=$(echo "${VERSION_JSON}" | jq -r .releaseTag)" >> "${GITHUB_OUTPUT}"
echo "RELEASE_VERSION=$(echo "${VERSION_JSON}" | jq -r .releaseVersion)" >> "${GITHUB_OUTPUT}"
echo "NPM_TAG=$(echo "${VERSION_JSON}" | jq -r .npmTag)" >> "${GITHUB_OUTPUT}"
echo "PREVIOUS_TAG=$(echo "${VERSION_JSON}" | jq -r .previousReleaseTag)" >> "${GITHUB_OUTPUT}"
- name: 'Print Calculated Version'
run: |-
echo "Calculated version: ${{ steps.version.outputs.RELEASE_VERSION }}"
- name: 'Run Tests'
uses: './.github/actions/run-tests'
with:
force_skip_tests: '${{ github.event.inputs.force_skip_tests }}'
gemini_api_key: '${{ secrets.GEMINI_API_KEY }}'
- name: 'Publish Release'
uses: './.github/actions/publish-release'
with:
release-version: '${{ steps.version.outputs.RELEASE_VERSION }}'
release-tag: '${{ steps.version.outputs.RELEASE_TAG }}'
npm-tag: '${{ steps.version.outputs.NPM_TAG }}'
wombat-token-core: '${{ secrets.WOMBAT_TOKEN_CORE }}'
wombat-token-cli: '${{ secrets.WOMBAT_TOKEN_CLI }}'
github-token: '${{ secrets.GITHUB_TOKEN }}'
dry-run: '${{ github.event.inputs.dry_run }}'
previous-tag: '${{ steps.version.outputs.PREVIOUS_TAG }}'
+11 -34
View File
@@ -8,6 +8,11 @@ on:
required: true required: true
type: 'boolean' type: 'boolean'
default: true default: true
force_skip_tests:
description: 'Select to skip the "Run Tests" step in testing. Prod releases should run tests'
required: false
type: 'boolean'
default: false
jobs: jobs:
calculate-versions: calculate-versions:
@@ -99,50 +104,22 @@ jobs:
working-directory: './release' working-directory: './release'
run: 'npm ci' run: 'npm ci'
- name: 'Configure Git User' - name: 'Run Tests'
working-directory: './release' uses: './.github/actions/run-tests'
run: |- with:
git config user.name "gemini-cli-robot" force_skip_tests: '${{ github.event.inputs.force_skip_tests }}'
git config user.email "gemini-cli-robot@google.com" gemini_api_key: '${{ secrets.GEMINI_API_KEY }}'
- name: 'Create and switch to a release branch'
working-directory: './release'
id: 'release_branch'
run: |
BRANCH_NAME="release/v${{ matrix.version }}"
git switch -c "${BRANCH_NAME}"
echo "BRANCH_NAME=${BRANCH_NAME}" >> "${GITHUB_OUTPUT}"
- name: 'Update package versions'
working-directory: './release'
run: 'npm run release:version "${{ matrix.version }}"'
- name: 'Commit and Conditionally Push package versions'
working-directory: './release'
env:
BRANCH_NAME: '${{ steps.release_branch.outputs.BRANCH_NAME }}'
DRY_RUN: '${{ github.event.inputs.dry_run }}'
RELEASE_TAG: 'v${{ matrix.version }}'
run: |-
git add package.json package-lock.json packages/*/package.json
git commit -m "chore(release): ${RELEASE_TAG}"
if [[ "${DRY_RUN}" == "false" ]]; then
echo "Pushing release branch to remote..."
git push --set-upstream origin "${BRANCH_NAME}" --follow-tags
else
echo "Dry run enabled. Skipping push."
fi
- name: 'Publish Release' - name: 'Publish Release'
uses: './.github/actions/publish-release' uses: './.github/actions/publish-release'
with: with:
release-version: '${{ matrix.version }}' release-version: '${{ matrix.version }}'
release-tag: 'v${{ matrix.version }}'
npm-tag: '${{ matrix.npm-tag }}' npm-tag: '${{ matrix.npm-tag }}'
wombat-token-core: '${{ secrets.WOMBAT_TOKEN_CORE }}' wombat-token-core: '${{ secrets.WOMBAT_TOKEN_CORE }}'
wombat-token-cli: '${{ secrets.WOMBAT_TOKEN_CLI }}' wombat-token-cli: '${{ secrets.WOMBAT_TOKEN_CLI }}'
github-token: '${{ secrets.GITHUB_TOKEN }}' github-token: '${{ secrets.GITHUB_TOKEN }}'
dry-run: '${{ github.event.inputs.dry_run }}' dry-run: '${{ github.event.inputs.dry_run }}'
release-branch: '${{ steps.release_branch.outputs.BRANCH_NAME }}'
previous-tag: '${{ matrix.previous-tag }}' previous-tag: '${{ matrix.previous-tag }}'
working-directory: './release' working-directory: './release'
+30 -22
View File
@@ -28,9 +28,7 @@ npm install -g @google/gemini-cli@latest
npm install -g @google/gemini-cli@nightly npm install -g @google/gemini-cli@nightly
``` ```
# Release Process. # Release Process
Where `x.y.z` is the next version to be released. In most all cases for the weekly release this will be an increment on `y`, aka minor version update. Major version updates `x` will need broader coordination and communication. For patches `z` see below. When possible we will do our best to adher to https://semver.org/
Our release cadence is new releases are sent to a preview channel for a week and then promoted to stable after a week. Version numbers will follow SemVer with weekly releases incrementing the minor version. Patches and bug fixes to both preview and stable releases will increment the patch version. Our release cadence is new releases are sent to a preview channel for a week and then promoted to stable after a week. Version numbers will follow SemVer with weekly releases incrementing the minor version. Patches and bug fixes to both preview and stable releases will increment the patch version.
@@ -38,23 +36,15 @@ Our release cadence is new releases are sent to a preview channel for a week and
Each night at UTC 0000 we will auto deploy a nightly release from `main`. This will be a version of the next production release, x.y.z, with the nightly tag. Each night at UTC 0000 we will auto deploy a nightly release from `main`. This will be a version of the next production release, x.y.z, with the nightly tag.
## Create Preview Release ## Weekly Release Promotion
Each Tuesday at UTC 2359 we will auto deploy a preview release of the next production release x.y.z. Each Tuesday, the on-call engineer will trigger the "Promote Release" workflow. This single action automates the entire weekly release process:
- This will happen as a scheduled instance of the release action. It will be cut off of Main. 1. **Promotes Preview to Stable:** The workflow identifies the latest `preview` release and promotes it to `stable`. This becomes the new `latest` version on npm.
- This will create a branch `release/vx.y.z-preview.n` 2. **Promotes Nightly to Preview:** The latest `nightly` release is then promoted to become the new `preview` version.
- We will run evals and smoke testing against this branch and the npm package. For now this should be manual smoke testing, we don't have a dedicated matrix or specific detailed process. There is work coming soon to make this more formalized and automatic see https://github.com/google-gemini/gemini-cli/issues/3788 3. **Prepares for next Nightly:** A pull request is automatically created and merged to bump the version in `main` in preparation for the next nightly release.
- Users installing `@preview` will get this release as well
## Promote Stable Release This process ensures a consistent and reliable release cadence with minimal manual intervention.
After one week (On the following Tuesday) with all signals a go, we will manually release at 2000 UTC via the current on-call person.
- The release action will be used with the source branch as `release/vx.y.z-preview.n`
- The version will be x.y.z
- The releaser will create and merge a pr into main with the version changes.
- Smoke tests and manual validation will be run. For now this should be manual smoke testing, we don't have a dedicated matrix or specific detailed process. There is work coming soon to make this more formalized and automatic see https://github.com/google-gemini/gemini-cli/issues/3788
## Patching Releases ## Patching Releases
@@ -79,7 +69,11 @@ The workflow will automatically find the merge commit SHA and begin the patch pr
**Option B: Manually Triggering the Workflow** **Option B: Manually Triggering the Workflow**
Navigate to the **Actions** tab and run the **Create Patch PR** workflow. Follow the manual release process using the "Patch Release" GitHub Actions workflow.
- **Type**: Select whether you are patching a `stable` or `preview` release. The workflow will automatically calculate the next patch version.
- **Ref**: Use your source branch as the reference (ex. `release/v0.2.0-preview.0`)
Navigate to the **Actions** tab and run the **Create Patch PR** workflow.
- **Commit**: The full SHA of the commit on `main` that you want to cherry-pick. - **Commit**: The full SHA of the commit on `main` that you want to cherry-pick.
- **Channel**: The channel you want to patch (`stable` or `preview`). - **Channel**: The channel you want to patch (`stable` or `preview`).
@@ -174,14 +168,28 @@ This fully automated process ensures that patches are created and released consi
## How To Release ## How To Release
Releases are managed through the [release.yml](https://github.com/google-gemini/gemini-cli/actions/workflows/release.yml) GitHub Actions workflow. To perform a manual release for a patch or hotfix: Releases are managed through GitHub Actions workflows.
### Weekly Promotions
To perform the weekly promotion of `preview` to `stable` and `nightly` to `preview`:
1. Navigate to the **Actions** tab of the repository. 1. Navigate to the **Actions** tab of the repository.
2. Select the **Release** workflow from the list. 2. Select the **Promote Release** workflow from the list.
3. Click the **Run workflow** dropdown button.
4. Leave **Dry Run** as `true` to test the workflow without publishing, or set to `false` to perform a live release.
5. Click **Run workflow**.
### Patching a Release
To perform a manual release for a patch or hotfix:
1. Navigate to the **Actions** tab of the repository.
2. Select the **Patch Release** workflow from the list.
3. Click the **Run workflow** dropdown button. 3. Click the **Run workflow** dropdown button.
4. Fill in the required inputs: 4. Fill in the required inputs:
- **Version**: The exact version to release (e.g., `v0.2.1`). - **Type**: Select whether you are patching a `stable` or `preview` release.
- **Ref**: The branch or commit SHA to release from (defaults to `main`). - **Ref**: The branch or commit SHA to release from.
- **Dry Run**: Leave as `true` to test the workflow without publishing, or set to `false` to perform a live release. - **Dry Run**: Leave as `true` to test the workflow without publishing, or set to `false` to perform a live release.
5. Click **Run workflow**. 5. Click **Run workflow**.
+1
View File
@@ -42,6 +42,7 @@
"lint": "eslint . --ext .ts,.tsx && eslint integration-tests", "lint": "eslint . --ext .ts,.tsx && eslint integration-tests",
"lint:fix": "eslint . --fix && eslint integration-tests --fix", "lint:fix": "eslint . --fix && eslint integration-tests --fix",
"lint:ci": "eslint . --ext .ts,.tsx --max-warnings 0 && eslint integration-tests --max-warnings 0", "lint:ci": "eslint . --ext .ts,.tsx --max-warnings 0 && eslint integration-tests --max-warnings 0",
"lint:all": "node scripts/lint.js",
"format": "prettier --experimental-cli --write .", "format": "prettier --experimental-cli --write .",
"typecheck": "npm run typecheck --workspaces --if-present", "typecheck": "npm run typecheck --workspaces --if-present",
"preflight": "npm run clean && npm ci && npm run format && npm run lint:ci && npm run build && npm run typecheck && npm run test:ci", "preflight": "npm run clean && npm ci && npm run format && npm run lint:ci && npm run build && npm run typecheck && npm run test:ci",
+35 -2
View File
@@ -47,8 +47,10 @@ export function getVersion(options = {}) {
const packageJson = JSON.parse( const packageJson = JSON.parse(
readFileSync(path.join(__dirname, '..', 'package.json'), 'utf-8'), readFileSync(path.join(__dirname, '..', 'package.json'), 'utf-8'),
); );
const [major, minor] = packageJson.version.split('.'); const versionParts = packageJson.version.split('.');
const nextMinor = parseInt(minor) + 1; const major = versionParts[0];
const minor = versionParts[1] ? parseInt(versionParts[1]) : 0;
const nextMinor = minor + 1;
const date = new Date().toISOString().slice(0, 10).replace(/-/g, ''); const date = new Date().toISOString().slice(0, 10).replace(/-/g, '');
const gitShortHash = execSync('git rev-parse --short HEAD') const gitShortHash = execSync('git rev-parse --short HEAD')
.toString() .toString()
@@ -71,6 +73,37 @@ export function getVersion(options = {}) {
latestNightlyTag.replace(/-nightly.*/, '').replace(/^v/, '') + '-preview'; latestNightlyTag.replace(/-nightly.*/, '').replace(/^v/, '') + '-preview';
npmTag = 'preview'; npmTag = 'preview';
previousReleaseTag = getLatestTag('contains("preview")'); previousReleaseTag = getLatestTag('contains("preview")');
} else if (type === 'patch') {
const patchFrom = options.patchFrom || args.patchFrom;
if (!patchFrom || (patchFrom !== 'stable' && patchFrom !== 'preview')) {
throw new Error(
'Patch type must be specified with --patch-from=stable or --patch-from=preview',
);
}
if (patchFrom === 'stable') {
previousReleaseTag = getLatestTag(
'(contains("nightly") or contains("preview")) | not',
);
const versionParts = previousReleaseTag.replace(/^v/, '').split('.');
const major = versionParts[0];
const minor = versionParts[1];
const patch = versionParts[2] ? parseInt(versionParts[2]) : 0;
releaseVersion = `${major}.${minor}.${patch + 1}`;
npmTag = 'latest';
} else {
// patchFrom === 'preview'
previousReleaseTag = getLatestTag('contains("preview")');
const [version, prerelease] = previousReleaseTag
.replace(/^v/, '')
.split('-');
const versionParts = version.split('.');
const major = versionParts[0];
const minor = versionParts[1];
const patch = versionParts[2] ? parseInt(versionParts[2]) : 0;
releaseVersion = `${major}.${minor}.${patch + 1}-${prerelease}`;
npmTag = 'preview';
}
} }
const releaseTag = `v${releaseVersion}`; const releaseTag = `v${releaseVersion}`;
+161
View File
@@ -0,0 +1,161 @@
#!/usr/bin/env node
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { execSync } from 'node:child_process';
import { mkdirSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
const ACTIONLINT_VERSION = '1.7.7';
const SHELLCHECK_VERSION = '0.11.0';
const YAMLLINT_VERSION = '1.35.1';
const TEMP_DIR = join(tmpdir(), 'gemini-cli-linters');
function getPlatformArch() {
const platform = process.platform;
const arch = process.arch;
if (platform === 'linux' && arch === 'x64') {
return {
actionlint: 'linux_amd64',
shellcheck: 'linux.x86_64',
};
}
if (platform === 'darwin' && arch === 'x64') {
return {
actionlint: 'darwin_amd64',
shellcheck: 'darwin.x86_64',
};
}
if (platform === 'darwin' && arch === 'arm64') {
return {
actionlint: 'darwin_arm64',
shellcheck: 'darwin.aarch64',
};
}
throw new Error(`Unsupported platform/architecture: ${platform}/${arch}`);
}
const platformArch = getPlatformArch();
/**
* @typedef {{
* check: string;
* installer: string;
* run: string;
* }} Linter
*/
/**
* @type {{[linterName: string]: Linter}}
*/
const LINTERS = {
actionlint: {
check: 'command -v actionlint',
installer: `
mkdir -p "${TEMP_DIR}/actionlint"
curl -sSLo "${TEMP_DIR}/.actionlint.tgz" "https://github.com/rhysd/actionlint/releases/download/v${ACTIONLINT_VERSION}/actionlint_${ACTIONLINT_VERSION}_${platformArch.actionlint}.tar.gz"
tar -xzf "${TEMP_DIR}/.actionlint.tgz" -C "${TEMP_DIR}/actionlint"
`,
run: `
actionlint \
-color \
-ignore 'SC2002:' \
-ignore 'SC2016:' \
-ignore 'SC2129:' \
-ignore 'label ".+" is unknown'
`,
},
shellcheck: {
check: 'command -v shellcheck',
installer: `
mkdir -p "${TEMP_DIR}/shellcheck"
curl -sSLo "${TEMP_DIR}/.shellcheck.txz" "https://github.com/koalaman/shellcheck/releases/download/v${SHELLCHECK_VERSION}/shellcheck-v${SHELLCHECK_VERSION}.${platformArch.shellcheck}.tar.xz"
tar -xf "${TEMP_DIR}/.shellcheck.txz" -C "${TEMP_DIR}/shellcheck" --strip-components=1
`,
run: `
git ls-files | grep -E '^([^.]+|.*\\.(sh|zsh|bash))' | xargs file --mime-type \
| grep "text/x-shellscript" | awk '{ print substr($1, 1, length($1)-1) }' \
| xargs shellcheck \
--check-sourced \
--enable=all \
--exclude=SC2002,SC2129,SC2310 \
--severity=style \
--format=gcc \
--color=never | sed -e 's/note:/warning:/g' -e 's/style:/warning:/g'
`,
},
yamllint: {
check: 'command -v yamllint',
installer: `pip3 install --user "yamllint==${YAMLLINT_VERSION}"`,
run: "git ls-files | grep -E '\\.(yaml|yml)' | xargs yamllint --format github",
},
};
function runCommand(command, stdio = 'inherit') {
try {
const env = { ...process.env };
env.PATH = `${TEMP_DIR}/actionlint:${TEMP_DIR}/shellcheck:${env.PATH}`;
if (process.platform === 'darwin') {
env.PATH = `${env.PATH}:${process.env.HOME}/Library/Python/3.12/bin`;
} else if (process.platform === 'linux') {
env.PATH = `${env.PATH}:${process.env.HOME}/.local/bin`;
}
execSync(command, { stdio, env });
return true;
} catch (_e) {
return false;
}
}
function setupLinters() {
console.log('Setting up linters...');
rmSync(TEMP_DIR, { recursive: true, force: true });
mkdirSync(TEMP_DIR, { recursive: true });
for (const linter in LINTERS) {
const { check, installer } = LINTERS[linter];
if (!runCommand(check, 'ignore')) {
console.log(`Installing ${linter}...`);
if (!runCommand(installer)) {
console.error(
`Failed to install ${linter}. Please install it manually.`,
);
process.exit(1);
}
}
}
console.log('All required linters are available.');
}
function runLinters() {
console.log('\nRunning ESLint...');
if (!runCommand('npm run lint:ci')) {
process.exit(1);
}
console.log('\nRunning actionlint...');
if (!runCommand(LINTERS.actionlint.run)) {
process.exit(1);
}
console.log('\nRunning shellcheck...');
if (!runCommand(LINTERS.shellcheck.run)) {
process.exit(1);
}
console.log('\nRunning yamllint...');
if (!runCommand(LINTERS.yamllint.run)) {
process.exit(1);
}
console.log('\nAll linting checks passed!');
}
setupLinters();
runLinters();
+61 -20
View File
@@ -7,28 +7,11 @@
import { vi, describe, it, expect, beforeEach } from 'vitest'; import { vi, describe, it, expect, beforeEach } from 'vitest';
import { getVersion } from '../get-release-version.js'; import { getVersion } from '../get-release-version.js';
import { execSync } from 'node:child_process'; import { execSync } from 'node:child_process';
import { readFileSync } from 'node:fs';
vi.mock('node:child_process'); vi.mock('node:child_process');
vi.mock('node:fs'); vi.mock('node:fs');
vi.mock('../get-release-version.js', async () => {
const actual = await vi.importActual('../get-release-version.js');
return {
...actual,
getVersion: (options) => {
if (options.type === 'nightly') {
return {
releaseTag: 'v0.6.0-nightly.20250911.a1b2c3d',
releaseVersion: '0.6.0-nightly.20250911.a1b2c3d',
npmTag: 'nightly',
previousReleaseTag: 'v0.5.0-nightly.20250910.abcdef',
};
}
return actual.getVersion(options);
},
};
});
describe('getReleaseVersion', () => { describe('getReleaseVersion', () => {
beforeEach(() => { beforeEach(() => {
vi.resetAllMocks(); vi.resetAllMocks();
@@ -37,14 +20,37 @@ describe('getReleaseVersion', () => {
}); });
describe('Nightly Workflow Logic', () => { describe('Nightly Workflow Logic', () => {
it('should calculate the next nightly version based on package.json', async () => { it('should calculate the next nightly version based on package.json', () => {
const { getVersion } = await import('../get-release-version.js'); vi.mocked(readFileSync).mockReturnValue('{"version": "0.5.0"}');
vi.mocked(execSync).mockImplementation((command) => {
if (command.includes('rev-parse')) return 'a1b2c3d';
if (command.includes('release list'))
return 'v0.5.0-nightly.20250910.abcdef';
return '';
});
const result = getVersion({ type: 'nightly' }); const result = getVersion({ type: 'nightly' });
expect(result.releaseVersion).toBe('0.6.0-nightly.20250911.a1b2c3d'); expect(result.releaseVersion).toBe('0.6.0-nightly.20250911.a1b2c3d');
expect(result.npmTag).toBe('nightly'); expect(result.npmTag).toBe('nightly');
expect(result.previousReleaseTag).toBe('v0.5.0-nightly.20250910.abcdef'); expect(result.previousReleaseTag).toBe('v0.5.0-nightly.20250910.abcdef');
}); });
it('should default minor to 0 if missing in package.json version', () => {
vi.mocked(readFileSync).mockReturnValue('{"version": "0"}');
vi.mocked(execSync).mockImplementation((command) => {
if (command.includes('rev-parse')) return 'a1b2c3d';
if (command.includes('release list'))
return 'v0.0.0-nightly.20250910.abcdef';
return '';
});
const result = getVersion({ type: 'nightly' });
expect(result.releaseVersion).toBe('0.1.0-nightly.20250911.a1b2c3d');
expect(result.npmTag).toBe('nightly');
expect(result.previousReleaseTag).toBe('v0.0.0-nightly.20250910.abcdef');
});
}); });
describe('Promote Workflow Logic', () => { describe('Promote Workflow Logic', () => {
@@ -82,4 +88,39 @@ describe('getReleaseVersion', () => {
expect(result.previousReleaseTag).toBe(latestPreview); expect(result.previousReleaseTag).toBe(latestPreview);
}); });
}); });
describe('Patch Workflow Logic', () => {
it('should calculate the next patch version for a stable release', () => {
const latestStable = 'v0.5.1';
vi.mocked(execSync).mockReturnValue(latestStable);
const result = getVersion({ type: 'patch', patchFrom: 'stable' });
expect(result.releaseVersion).toBe('0.5.2');
expect(result.npmTag).toBe('latest');
expect(result.previousReleaseTag).toBe(latestStable);
});
it('should calculate the next patch version for a preview release', () => {
const latestPreview = 'v0.6.0-preview';
vi.mocked(execSync).mockReturnValue(latestPreview);
const result = getVersion({ type: 'patch', patchFrom: 'preview' });
expect(result.releaseVersion).toBe('0.6.1-preview');
expect(result.npmTag).toBe('preview');
expect(result.previousReleaseTag).toBe(latestPreview);
});
it('should default patch to 0 if missing in stable release', () => {
const latestStable = 'v0.5';
vi.mocked(execSync).mockReturnValue(latestStable);
const result = getVersion({ type: 'patch', patchFrom: 'stable' });
expect(result.releaseVersion).toBe('0.5.1');
expect(result.npmTag).toBe('latest');
expect(result.previousReleaseTag).toBe(latestStable);
});
});
}); });