Skip to content

chore: weekly release 2026-03-27 #8

chore: weekly release 2026-03-27

chore: weekly release 2026-03-27 #8

Workflow file for this run

name: Release npm (JS & CLI)
on:
issue_comment:
types: [created]
workflow_dispatch:
inputs:
package:
description: 'Package to release (cli or js)'
required: true
type: choice
options:
- cli
- js
release_type:
description: 'Release type'
required: true
type: choice
options:
- patch
- minor
- major
prerelease:
description: 'Prerelease tag (leave empty for stable release)'
required: false
type: choice
options:
- ''
- beta
- alpha
- rc
ref:
description: 'Branch/ref to release from (default: main)'
required: false
type: string
default: 'main'
permissions:
contents: write
issues: write
pull-requests: write
id-token: write # Required for npm Trusted Publishers (OIDC)
env:
NODE_VERSION: '20'
jobs:
parse:
if: >
github.event_name == 'issue_comment' &&
(contains(github.event.comment.body, '!release') ||
contains(github.event.comment.body, '!Release') ||
contains(github.event.comment.body, '!RELEASE')) &&
github.event.comment.user.type != 'Bot'
runs-on: ubuntu-latest
outputs:
should_release: ${{ steps.parse.outputs.should_release }}
package: ${{ steps.parse.outputs.package }}
release_type: ${{ steps.parse.outputs.release_type }}
prerelease: ${{ steps.parse.outputs.prerelease }}
npm_tag: ${{ steps.parse.outputs.npm_tag }}
ref: ${{ steps.parse.outputs.ref }}
pr_branch: ${{ steps.parse.outputs.pr_branch }}
pr_title: ${{ steps.parse.outputs.pr_title }}
pr_url: ${{ steps.parse.outputs.pr_url }}
issue_number: ${{ steps.parse.outputs.issue_number }}
requested_by: ${{ steps.parse.outputs.requested_by }}
request_url: ${{ steps.parse.outputs.request_url }}
steps:
- name: Parse release command
id: parse
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const allowedPackages = ['cli', 'js'];
const allowedBumps = ['patch', 'minor', 'major'];
const allowedPrereleaseTags = ['beta', 'alpha', 'rc'];
const body = context.payload.comment.body.trim();
const match = body.match(/!release\s+(\S+)\s+(\S+)(?:\s+(\S+))?/i);
if (!match) {
return;
}
const packageName = match[1].toLowerCase();
const releaseType = match[2].toLowerCase();
const prereleaseTag = match[3] ? match[3].toLowerCase() : '';
const allowedAssociation = ['OWNER', 'MEMBER', 'COLLABORATOR'];
const association = context.payload.comment.author_association;
const actor = context.payload.comment.user.login;
const owner = context.repo.owner;
const repo = context.repo.repo;
const issueNumber = context.payload.issue.number;
// python/py packages are handled by release-python.yml β€” skip silently
const pythonPackages = ['python', 'py'];
if (pythonPackages.includes(packageName)) {
return;
}
// go/golang packages are handled by release-go.yml β€” skip silently
const goPackages = ['go', 'golang'];
if (goPackages.includes(packageName)) {
return;
}
const invalidPackage = !allowedPackages.includes(packageName);
const invalidBump = !allowedBumps.includes(releaseType);
const invalidPrerelease = prereleaseTag && !allowedPrereleaseTags.includes(prereleaseTag);
if (invalidPackage || invalidBump || invalidPrerelease) {
const message = [
`⚠️ @${actor} I couldn't parse the release command.`,
'',
'Expected format: `!release <cli|js|python|go> <patch|minor|major> [beta|alpha|rc]`',
'',
'Examples:',
'- `!release cli patch` - stable release',
'- `!release js minor beta` - beta release',
'- `!release python minor` - Python SDK release',
'- `!release go minor` - Go SDK release'
];
await github.rest.issues.createComment({
owner,
repo,
issue_number: issueNumber,
body: message.join('\n')
});
return;
}
if (!allowedAssociation.includes(association)) {
const message = [
`β›” @${actor} you do not have permission to trigger releases.`,
'',
'Please contact the repository maintainers if this is unexpected.'
];
await github.rest.issues.createComment({
owner,
repo,
issue_number: issueNumber,
body: message.join('\n')
});
return;
}
let ref = 'main';
let prBranch = '';
let prTitle = '';
let prUrl = '';
if (context.payload.issue.pull_request) {
const { data: pr } = await github.rest.pulls.get({
owner,
repo,
pull_number: issueNumber
});
if (!pr.head.repo || pr.head.repo.full_name !== `${owner}/${repo}`) {
const message = [
`β›” @${actor} releases from forked branches are not supported.`,
'',
'Please push the branch to this repository or run the release manually in Actions.'
];
await github.rest.issues.createComment({
owner,
repo,
issue_number: issueNumber,
body: message.join('\n')
});
return;
}
ref = pr.head.ref;
prBranch = pr.head.ref;
prTitle = pr.title;
prUrl = pr.html_url;
}
const npmTag = prereleaseTag || 'latest';
const releaseTypeLabel = prereleaseTag ? `${releaseType} (${prereleaseTag})` : releaseType;
const lines = [
`πŸš€ @${actor} release command accepted: \`${packageName}\` \`${releaseTypeLabel}\`.`,
'',
'The release workflow is queued; results will be posted here.'
];
if (prBranch) {
lines.splice(2, 0, `Target branch: \`${prBranch}\` (open PR). Version commits will be pushed to this branch.`);
}
if (prereleaseTag) {
lines.splice(2, 0, `πŸ“¦ Prerelease tag: \`${prereleaseTag}\` (will publish to npm with tag \`${npmTag}\`)`);
}
const message = lines.join('\n');
await github.rest.issues.createComment({
owner,
repo,
issue_number: issueNumber,
body: message
});
core.setOutput('should_release', 'true');
core.setOutput('package', packageName);
core.setOutput('release_type', releaseType);
core.setOutput('prerelease', prereleaseTag);
core.setOutput('npm_tag', npmTag);
core.setOutput('ref', ref);
core.setOutput('pr_branch', prBranch);
core.setOutput('pr_title', prTitle);
core.setOutput('pr_url', prUrl);
core.setOutput('issue_number', issueNumber);
core.setOutput('requested_by', actor);
core.setOutput('request_url', context.payload.comment.html_url);
release:
needs: [parse]
if: |
always() &&
(
(needs.parse.result == 'success' && needs.parse.outputs.should_release == 'true') ||
(github.event_name == 'workflow_dispatch')
)
concurrency:
group: release-${{ github.event_name == 'workflow_dispatch' && inputs.package || needs.parse.outputs.package }}-${{ github.event_name == 'workflow_dispatch' && inputs.ref || needs.parse.outputs.ref }}
cancel-in-progress: false
runs-on: ubuntu-latest
env:
PACKAGE: ${{ github.event_name == 'workflow_dispatch' && inputs.package || needs.parse.outputs.package }}
RELEASE_TYPE: ${{ github.event_name == 'workflow_dispatch' && inputs.release_type || needs.parse.outputs.release_type }}
PRERELEASE: ${{ github.event_name == 'workflow_dispatch' && inputs.prerelease || needs.parse.outputs.prerelease }}
NPM_TAG: ${{ github.event_name == 'workflow_dispatch' && (inputs.prerelease || 'latest') || needs.parse.outputs.npm_tag }}
SOURCE_REF: ${{ github.event_name == 'workflow_dispatch' && inputs.ref || needs.parse.outputs.ref }}
ISSUE_NUMBER: ${{ needs.parse.outputs.issue_number }}
REQUESTED_BY: ${{ github.event_name == 'workflow_dispatch' && github.actor || needs.parse.outputs.requested_by }}
REQUEST_URL: ${{ needs.parse.outputs.request_url }}
PR_TITLE: ${{ needs.parse.outputs.pr_title }}
PR_URL: ${{ needs.parse.outputs.pr_url }}
PR_BRANCH: ${{ needs.parse.outputs.pr_branch }}
steps:
- name: Validate context
run: |
set -e
if [ -z "${PACKAGE:-}" ]; then
echo "PACKAGE is not set"
exit 1
fi
case "$PACKAGE" in
cli|js) ;;
*)
echo "Unsupported package: $PACKAGE"
exit 1
;;
esac
if [ -z "${RELEASE_TYPE:-}" ]; then
echo "RELEASE_TYPE is not set"
exit 1
fi
case "$RELEASE_TYPE" in
patch|minor|major) ;;
*)
echo "Unsupported release type: $RELEASE_TYPE"
exit 1
;;
esac
if [ -z "${SOURCE_REF:-}" ]; then
echo "SOURCE_REF is not set"
exit 1
fi
- name: Checkout source
uses: actions/checkout@v4
with:
fetch-depth: 0
ref: ${{ env.SOURCE_REF }}
- name: Configure git
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
registry-url: https://registry.npmjs.org/
- name: Setup Bun
uses: oven-sh/setup-bun@v2
- name: Install workspace dependencies
run: bun install
- name: Build js SDK (dependency for cli)
if: env.PACKAGE == 'cli'
working-directory: js
run: bun run build
- name: Bump package version
id: bump
run: node scripts/bump-version.js "$PACKAGE" "$RELEASE_TYPE" "$PRERELEASE"
- name: Format package.json
working-directory: ${{ env.PACKAGE }}
run: bun run fmt
- name: Export version metadata
run: |
echo "NEW_VERSION=${{ steps.bump.outputs.version }}" >> "$GITHUB_ENV"
echo "TAG_NAME=${PACKAGE}-v${{ steps.bump.outputs.version }}" >> "$GITHUB_ENV"
- name: Capture package metadata
id: pkgmeta
run: |
npm_name=$(node -e "console.log(require('./${PACKAGE}/package.json').name)")
npm_url=$(node -e "const name = require('./${PACKAGE}/package.json').name; console.log('https://www.npmjs.com/package/' + encodeURIComponent(name));")
echo "npm_name=$npm_name" >> "$GITHUB_OUTPUT"
echo "npm_url=$npm_url" >> "$GITHUB_OUTPUT"
- name: Run lint
working-directory: ${{ env.PACKAGE }}
run: bun run lint
- name: Run type check
working-directory: ${{ env.PACKAGE }}
run: bun run type-check
- name: Build package
working-directory: ${{ env.PACKAGE }}
run: bun run build
- name: Run tests
working-directory: ${{ env.PACKAGE }}
run: bun run test
- name: Analyze package contents
id: pack_info
working-directory: ${{ env.PACKAGE }}
run: |
npm pack --dry-run > pack_output.txt 2>&1
# Extract package size info
package_size=$(grep "package size:" pack_output.txt | awk '{print $4, $5}')
unpacked_size=$(grep "unpacked size:" pack_output.txt | awk '{print $4, $5}')
total_files=$(grep "total files:" pack_output.txt | awk '{print $4}')
# Get file list (skip the first few lines and last few lines of npm notice)
file_list=$(sed -n '/Tarball Contents/,/Tarball Details/p' pack_output.txt | \
grep "npm notice" | \
grep -v "Tarball Contents" | \
grep -v "Tarball Details" | \
sed 's/npm notice //g' | \
head -20)
# Save to GitHub output (escape newlines for multiline output)
echo "package_size=${package_size}" >> "$GITHUB_OUTPUT"
echo "unpacked_size=${unpacked_size}" >> "$GITHUB_OUTPUT"
echo "total_files=${total_files}" >> "$GITHUB_OUTPUT"
# Save file list with proper multiline format
echo "file_list<<EOF" >> "$GITHUB_OUTPUT"
echo "$file_list" >> "$GITHUB_OUTPUT"
echo "EOF" >> "$GITHUB_OUTPUT"
# Clean up
rm pack_output.txt
- name: Generate changelog
working-directory: ${{ env.PACKAGE }}
run: |
npm install -g conventional-changelog-cli conventional-changelog-conventionalcommits
# Update CHANGELOG.md (prepends latest release)
conventional-changelog -p conventionalcommits \
--commit-path . \
-i CHANGELOG.md \
-s \
--pkg-path ./package.json \
-n ../changelog.config.js \
--tag-prefix "${PACKAGE}-v"
# Generate release notes for GitHub Release body
conventional-changelog -p conventionalcommits \
--commit-path . \
-o RELEASE_NOTES.md \
--pkg-path ./package.json \
-n ../changelog.config.js \
--tag-prefix "${PACKAGE}-v"
- name: Commit version change
run: |
git add "${PACKAGE}/package.json"
git add "${PACKAGE}/CHANGELOG.md"
if [ -f "${PACKAGE}/package-lock.json" ]; then
git add "${PACKAGE}/package-lock.json"
fi
if [ -f "${PACKAGE}/bun.lock" ]; then
git add "${PACKAGE}/bun.lock"
fi
if [ -f "${PACKAGE}/bun.lockb" ]; then
git add "${PACKAGE}/bun.lockb"
fi
if git diff --cached --quiet; then
echo "No changes to commit"
exit 1
fi
git commit -m "chore(${PACKAGE}): release v${NEW_VERSION}"
- name: Resolve workspace dependencies
working-directory: ${{ env.PACKAGE }}
run: |
# Get the version of @phala/cloud from js/package.json
if grep -q '"@phala/cloud": "workspace:\*"' package.json; then
JS_VERSION=$(node -e "console.log(require('../js/package.json').version)")
echo "Resolving workspace:* to version $JS_VERSION"
node -e "
const fs = require('fs');
const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8'));
if (pkg.dependencies && pkg.dependencies['@phala/cloud'] === 'workspace:*') {
pkg.dependencies['@phala/cloud'] = '^$JS_VERSION';
fs.writeFileSync('package.json', JSON.stringify(pkg, null, 2) + '\n');
}
"
fi
- name: Upgrade npm for trusted publishing
run: |
# Ensure npm 11.5.1 or later for OIDC trusted publishing support
npm install -g npm@latest
npm --version
- name: Verify OIDC token and npm authentication
run: |
echo "πŸ” Checking OIDC token availability..."
if [ -n "${ACTIONS_ID_TOKEN_REQUEST_URL}" ] && [ -n "${ACTIONS_ID_TOKEN_REQUEST_TOKEN}" ]; then
echo "βœ… OIDC token environment variables are set"
echo " ACTIONS_ID_TOKEN_REQUEST_URL: ${ACTIONS_ID_TOKEN_REQUEST_URL}"
else
echo "❌ OIDC token environment variables are NOT set"
echo " This indicates that id-token: write permission may not be working"
fi
echo ""
echo "πŸ” Checking npm whoami (should work with OIDC)..."
if npm whoami 2>&1 | grep -q "npm ERR!"; then
echo "⚠️ npm whoami failed (expected for OIDC - no pre-auth needed)"
else
echo "βœ… npm authenticated as: $(npm whoami)"
fi
echo ""
echo "πŸ“‹ npm config:"
npm config list
echo ""
echo "πŸ” GitHub Actions context:"
echo " Repository: ${{ github.repository }}"
echo " Workflow: ${{ github.workflow }}"
echo " Run ID: ${{ github.run_id }}"
echo " Actor: ${{ github.actor }}"
- name: Publish to npm
working-directory: ${{ env.PACKAGE }}
run: |
echo "πŸš€ Publishing @phala/cloud..."
echo ""
echo "Package info:"
cat package.json | grep -E '"name"|"version"'
echo ""
echo "πŸ“‹ Publish command: npm publish --access public --tag $NPM_TAG"
echo ""
# Publish using OIDC authentication (npm Trusted Publishers)
# Requires npm >= 11.5.1 and id-token: write permission
# Provenance attestations are generated automatically
npm publish --access public --tag "$NPM_TAG" --verbose 2>&1 | tee publish.log || {
echo ""
echo "❌ Publish failed!"
echo ""
echo "πŸ” Checking authentication method used by npm..."
if grep -q "oidc" publish.log || grep -q "OIDC" publish.log; then
echo " npm attempted to use OIDC authentication"
elif grep -q "token" publish.log || grep -q "TOKEN" publish.log; then
echo " npm attempted to use token authentication"
fi
echo ""
echo "πŸ” Checking Trusted Publisher configuration..."
echo " Expected configuration on npmjs.com:"
echo " - Package: @phala/cloud"
echo " - Provider: GitHub Actions"
echo " - Owner: Phala-Network"
echo " - Repository: phala-cloud"
echo " - Workflow: release-npm.yml"
echo " - Environment: (empty)"
echo ""
echo " Actual workflow context:"
echo " - Repository: ${{ github.repository }}"
echo " - Workflow: ${{ github.workflow }}"
echo " - Workflow file: ${{ github.workflow_ref }}"
exit 1
}
rm -f publish.log
- name: Create tag
run: git tag "$TAG_NAME"
- name: Push changes
id: push
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
if [ "$SOURCE_REF" = "main" ]; then
# Main branch has protection rules - create a release branch and PR
RELEASE_BRANCH="release/${PACKAGE}-v${NEW_VERSION}"
git checkout -b "$RELEASE_BRANCH"
git push origin "$RELEASE_BRANCH"
git push origin "$TAG_NAME"
# Create PR to merge release branch back to main
PR_URL=$(gh pr create \
--base main \
--head "$RELEASE_BRANCH" \
--title "chore(${PACKAGE}): release v${NEW_VERSION}" \
--body "Automated release PR for ${PACKAGE} v${NEW_VERSION}. Updates package.json and CHANGELOG.md. The package has already been published to npm.")
echo "release_pr_url=$PR_URL" >> "$GITHUB_OUTPUT"
echo "used_release_branch=true" >> "$GITHUB_OUTPUT"
echo "release_branch=$RELEASE_BRANCH" >> "$GITHUB_OUTPUT"
else
# Feature branch - push directly
git push origin HEAD:${SOURCE_REF}
git push origin "$TAG_NAME"
echo "used_release_branch=false" >> "$GITHUB_OUTPUT"
fi
- name: Create GitHub release
id: release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
# Build release body: changelog + install info
{
if [ -s "${PACKAGE}/RELEASE_NOTES.md" ]; then
cat "${PACKAGE}/RELEASE_NOTES.md"
else
echo "Release ${PACKAGE} v${NEW_VERSION}"
fi
echo ""
echo "---"
echo ""
echo "**npm**: [${{ steps.pkgmeta.outputs.npm_name }}@${NEW_VERSION}](${{ steps.pkgmeta.outputs.npm_url }})"
echo ""
echo "\`\`\`bash"
echo "npm i -g ${{ steps.pkgmeta.outputs.npm_name }}@${NEW_VERSION}"
echo "\`\`\`"
} > release_body.md
PRERELEASE_FLAG=""
if [ -n "$PRERELEASE" ]; then
PRERELEASE_FLAG="--prerelease"
fi
RELEASE_URL=$(gh release create "$TAG_NAME" \
--title "${{ steps.pkgmeta.outputs.npm_name }} v${NEW_VERSION}" \
--notes-file release_body.md \
$PRERELEASE_FLAG)
echo "html_url=$RELEASE_URL" >> "$GITHUB_OUTPUT"
- name: Comment success
if: ${{ success() && env.ISSUE_NUMBER != '' }}
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
const fileList = `${{ steps.pack_info.outputs.file_list }}`.split('\n').slice(0, 15).join('\n');
const totalFiles = Number('${{ steps.pack_info.outputs.total_files }}');
const hasMore = totalFiles > 15;
const usedReleaseBranch = '${{ steps.push.outputs.used_release_branch }}' === 'true';
const releasePrUrl = '${{ steps.push.outputs.release_pr_url }}';
const body = [
`πŸŽ‰ Release completed: \`${process.env.PACKAGE}\` v${process.env.NEW_VERSION}`,
'',
`- Branch: \`${process.env.SOURCE_REF}\``,
`- npm: [${{ steps.pkgmeta.outputs.npm_name }}](${{ steps.pkgmeta.outputs.npm_url }})`,
`- GitHub Release: [link](${{ steps.release.outputs.html_url }})`,
`- Workflow logs: ${runUrl}`,
];
if (usedReleaseBranch && releasePrUrl) {
body.push('');
body.push(`> ⚠️ **Action Required**: Please merge the [release PR](${releasePrUrl}) to update \`main\` branch with version changes.`);
}
body.push(
'',
'### πŸ“¦ Package Info',
`- Package size: **${{ steps.pack_info.outputs.package_size }}**`,
`- Unpacked size: **${{ steps.pack_info.outputs.unpacked_size }}**`,
`- Total files: **${{ steps.pack_info.outputs.total_files }}**`,
'',
'<details>',
`<summary>πŸ“„ Files included${hasMore ? ' (showing first 15)' : ''}</summary>`,
'',
'```',
fileList,
hasMore ? `... and ${totalFiles - 15} more files` : '',
'```',
'</details>'
);
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: Number(process.env.ISSUE_NUMBER),
body: body.join('\n')
});
- name: Comment failure
if: ${{ failure() && env.ISSUE_NUMBER != '' }}
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
const actor = process.env.REQUESTED_BY ? `@${process.env.REQUESTED_BY}` : 'Requester';
const version = process.env.NEW_VERSION || 'unknown version';
const body = [
`❌ ${actor} release failed: \`${process.env.PACKAGE}\` ${version}`,
'',
`Branch: \`${process.env.SOURCE_REF}\``,
`Please review the workflow logs: ${runUrl}`
].join('\n');
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: Number(process.env.ISSUE_NUMBER),
body
});