Skip to content

codexmate auto

codexmate auto #45

Workflow file for this run

name: release
run-name: "${{ github.event.repository.name }} ${{ inputs.tag || 'auto' }}"
on:
workflow_dispatch:
inputs:
tag:
description: 'Tag (e.g., v0.0.1). Empty = use package.json version.'
required: false
default: ''
permissions:
contents: write
jobs:
release:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
fetch-tags: true
- name: Fetch tags
run: git fetch --tags --force
- uses: actions/setup-node@v4
with:
node-version: '18'
cache: 'npm'
- name: Resolve release tag
id: resolve
env:
INPUT_TAG: ${{ inputs.tag }}
run: |
node - <<'NODE'
const { execSync } = require('child_process');
const fs = require('fs');
const inputTagRaw = (process.env.INPUT_TAG || '').trim();
const normalizeTag = (tag) => tag.startsWith('v') ? tag.slice(1) : tag;
const isSemver = (version) => /^\d+\.\d+\.\d+$/.test(version);
const parseSemver = (version) => {
if (!isSemver(version)) return null;
const [major, minor, patch] = version.split('.').map(n => Number(n));
return { major, minor, patch };
};
const compareSemver = (a, b) => {
const pa = parseSemver(a);
const pb = parseSemver(b);
if (!pa || !pb) return 0;
if (pa.major !== pb.major) return pa.major - pb.major;
if (pa.minor !== pb.minor) return pa.minor - pb.minor;
return pa.patch - pb.patch;
};
const hasTag = (tag) => {
if (!tag) return false;
try {
execSync(`git show-ref --tags --verify --quiet refs/tags/${tag}`, { stdio: 'ignore' });
return true;
} catch (e) {
return false;
}
};
let latestTag = '';
try {
const tagOutput = execSync("git tag --list v* --sort=-v:refname", { encoding: 'utf8' }).trim();
latestTag = tagOutput.split(/\r?\n/).find(Boolean) || '';
} catch (e) {
latestTag = '';
}
const pkg = require('./package.json');
const pkgVersion = pkg.version;
if (!isSemver(pkgVersion)) {
console.error(`package.json version ${pkgVersion} is not a valid semver.`);
process.exit(1);
}
const latestVersion = latestTag ? normalizeTag(latestTag) : '';
const latestSemver = latestVersion ? parseSemver(latestVersion) : null;
let resolvedTag = '';
let expectedVersion = '';
let mode = '';
let baseVersion = '';
let baseSource = '';
let tagExists = false;
if (inputTagRaw) {
if (!/^v?\d+\.\d+\.\d+$/.test(inputTagRaw)) {
console.error('Invalid tag format. Use vX.Y.Z or X.Y.Z.');
process.exit(1);
}
resolvedTag = inputTagRaw.startsWith('v') ? inputTagRaw : `v${inputTagRaw}`;
expectedVersion = normalizeTag(resolvedTag);
mode = 'manual';
tagExists = hasTag(resolvedTag);
baseVersion = tagExists ? expectedVersion : '';
baseSource = tagExists ? 'input_tag' : 'input_tag_new';
} else {
mode = 'auto';
if (latestTag && !latestSemver) {
console.error(`Latest tag ${latestTag} is not a valid semver.`);
process.exit(1);
}
if (latestSemver && compareSemver(pkgVersion, latestVersion) < 0) {
console.error(`package.json version ${pkgVersion} is lower than latest tag ${latestTag}.`);
process.exit(1);
}
resolvedTag = `v${pkgVersion}`;
expectedVersion = pkgVersion;
tagExists = hasTag(resolvedTag);
baseVersion = latestVersion;
baseSource = tagExists ? 'package_tag' : 'package_version';
}
const envLines = [
`RELEASE_TAG=${resolvedTag}`,
`RELEASE_VERSION=${expectedVersion}`,
`RELEASE_MODE=${mode}`,
`LATEST_TAG=${latestTag}`,
`PACKAGE_VERSION=${pkgVersion}`,
`BASE_VERSION=${baseVersion}`,
`BASE_SOURCE=${baseSource}`,
`TAG_EXISTS=${tagExists ? 'true' : 'false'}`
].join('\n') + '\n';
fs.appendFileSync(process.env.GITHUB_ENV, envLines);
const outputLines = [
`release_tag=${resolvedTag}`,
`release_version=${expectedVersion}`,
`release_mode=${mode}`,
`latest_tag=${latestTag}`,
`package_version=${pkgVersion}`,
`base_version=${baseVersion}`,
`base_source=${baseSource}`,
`tag_exists=${tagExists ? 'true' : 'false'}`
].join('\n') + '\n';
fs.appendFileSync(process.env.GITHUB_OUTPUT, outputLines);
const summaryLines = [
'### Release Preview',
`- mode: ${mode}`,
`- input_tag: ${inputTagRaw || '(empty)'}`,
`- latest_tag: ${latestTag || '(none)'}`,
`- package_version: ${pkgVersion}`,
`- base_version: ${baseVersion || '(none)'}`,
`- base_source: ${baseSource || '(none)'}`,
`- resolved_tag: ${resolvedTag}`,
`- tag_exists: ${tagExists ? 'yes' : 'no'}`,
`- expected_version: ${expectedVersion}`
].join('\n');
fs.appendFileSync(process.env.GITHUB_STEP_SUMMARY, summaryLines + '\n');
console.log(`::notice title=Resolved Tag::${resolvedTag}`);
NODE
- name: Checkout target tag
if: ${{ steps.resolve.outputs.tag_exists == 'true' }}
env:
RELEASE_TAG: ${{ steps.resolve.outputs.release_tag }}
run: |
git rev-parse "refs/tags/${RELEASE_TAG}" >/dev/null 2>&1
git checkout "${RELEASE_TAG}"
- name: Verify tag matches package.json version
if: ${{ steps.resolve.outputs.tag_exists == 'true' }}
env:
RELEASE_TAG: ${{ steps.resolve.outputs.release_tag }}
run: |
node -e "const pkg=require('./package.json'); const tag=process.env.RELEASE_TAG; const expected='v'+pkg.version; if(tag!==expected){ console.error('Tag '+tag+' does not match package.json version '+expected); process.exit(1);} console.log('Tag matches '+expected);"
- name: Verify package.json matches release tag
if: ${{ steps.resolve.outputs.tag_exists != 'true' }}
env:
RELEASE_TAG: ${{ steps.resolve.outputs.release_tag }}
run: |
node -e "const pkg=require('./package.json'); const tag=process.env.RELEASE_TAG; const expected='v'+pkg.version; if(tag!==expected){ console.error('Current commit package.json '+expected+' does not match resolved release tag '+tag); process.exit(1);} console.log('Current package matches '+expected);"
- name: Compute release name
env:
RELEASE_TAG: ${{ steps.resolve.outputs.release_tag }}
run: |
node -e "const p=require('./package.json'); const tag=process.env.RELEASE_TAG; const name=p.name.includes('/')? p.name.split('/')[1]: p.name; const value=name+' '+tag; console.log('RELEASE_NAME='+value);" >> "$GITHUB_ENV"
- name: Pack npm artifact
run: |
name=$(node -e "const p=require('./package.json'); const n=p.name.replace('@','').replace('/','-'); process.stdout.write(n+'-'+p.version+'.tgz');")
npm pack
test -f "$name"
echo "PACKAGE_TGZ=$name" >> "$GITHUB_ENV"
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ steps.resolve.outputs.release_tag }}
target_commitish: ${{ github.sha }}
name: ${{ env.RELEASE_NAME }}
prerelease: false
draft: false
files: ${{ env.PACKAGE_TGZ }}
generate_release_notes: true