Skip to content

fix: persist setup wizard choice commits (#34) #5

fix: persist setup wizard choice commits (#34)

fix: persist setup wizard choice commits (#34) #5

Workflow file for this run

name: Publish Package
on:
push:
branches: [main]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: false
permissions:
contents: write
pull-requests: write
id-token: write
jobs:
resolve-merge-context:
name: Resolve Merge Context
runs-on: blacksmith-2vcpu-ubuntu-2404
permissions:
contents: read
pull-requests: read
outputs:
pr_number: ${{ steps.context.outputs.pr_number }}
pr_url: ${{ steps.context.outputs.pr_url }}
pr_title: ${{ steps.context.outputs.pr_title }}
pr_head_ref: ${{ steps.context.outputs.pr_head_ref }}
is_bump_pr: ${{ steps.context.outputs.is_bump_pr }}
steps:
- name: Ensure push commit came from merged PR
id: context
uses: actions/github-script@v7
with:
script: |
const query = `
query($owner: String!, $repo: String!, $oid: GitObjectID!) {
repository(owner: $owner, name: $repo) {
object(oid: $oid) {
... on Commit {
oid
associatedPullRequests(first: 10) {
nodes {
number
state
mergedAt
url
title
headRefName
}
}
}
}
}
}
`;
const maxAttempts = 6;
const retryDelayMs = 5000;
const findMergedPr = async () => {
const data = await github.graphql(query, {
owner: context.repo.owner,
repo: context.repo.repo,
oid: context.sha,
});
const commit = data.repository?.object;
if (!commit) {
throw new Error(`Could not load commit ${context.sha}`);
}
const pullRequests = commit.associatedPullRequests?.nodes ?? [];
return pullRequests.find(
(pr) =>
pr.state === "MERGED" &&
typeof pr.mergedAt === "string" &&
pr.mergedAt.length > 0,
);
};
let mergedPr;
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
mergedPr = await findMergedPr();
if (mergedPr) {
break;
}
if (attempt < maxAttempts) {
core.info(
`No merged PR associated with ${context.sha} yet (attempt ${attempt}/${maxAttempts}); retrying in ${retryDelayMs / 1000}s.`,
);
await new Promise((resolve) => setTimeout(resolve, retryDelayMs));
}
}
if (!mergedPr) {
core.setFailed(
`Direct push detected on main at ${context.sha}. Publish flow requires merged PR commits only.`,
);
return;
}
core.info(
`Publish gate passed via merged PR #${mergedPr.number} (${mergedPr.url})`,
);
const isBumpPr =
(mergedPr.headRefName ?? "").startsWith("ci/version-bump-") ||
/^chore:\s*bump package version to 0\.0\.\d+$/i.test(
mergedPr.title ?? "",
);
core.setOutput("pr_number", String(mergedPr.number));
core.setOutput("pr_url", mergedPr.url ?? "");
core.setOutput("pr_title", mergedPr.title ?? "");
core.setOutput("pr_head_ref", mergedPr.headRefName ?? "");
core.setOutput("is_bump_pr", isBumpPr ? "true" : "false");
core.info(
`Resolved publish mode: ${isBumpPr ? "stable release (latest)" : "prerelease (next)"}`,
);
publish-and-manage-bump:
name: Publish Package + Manage Bump PR
runs-on: ubuntu-latest
needs: resolve-merge-context
permissions:
contents: write
pull-requests: write
id-token: write
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: "22.14.0"
registry-url: "https://registry.npmjs.org"
- name: Upgrade npm for trusted publishing support
run: npm install -g npm@latest
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: 1.3.10
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Validate package
run: |
bun run check
bun run typecheck
bun test
bun run build
- name: Report publish strategy
run: |
echo "Merged PR #${{ needs.resolve-merge-context.outputs.pr_number }} (${{ needs.resolve-merge-context.outputs.pr_url }})"
echo "Head ref: ${{ needs.resolve-merge-context.outputs.pr_head_ref }}"
echo "Title: ${{ needs.resolve-merge-context.outputs.pr_title }}"
echo "Bump PR: ${{ needs.resolve-merge-context.outputs.is_bump_pr }}"
- name: Resolve package metadata
id: meta
env:
IS_BUMP_PR: ${{ needs.resolve-merge-context.outputs.is_bump_pr }}
run: |
node <<'NODE'
const fs = require("node:fs");
const pkg = JSON.parse(fs.readFileSync("package.json", "utf8"));
const match = /^0\.0\.(\d+)$/.exec(pkg.version ?? "");
const isBumpPr = process.env.IS_BUMP_PR === "true";
const runNumber = process.env.GITHUB_RUN_NUMBER ?? "0";
const runAttempt = process.env.GITHUB_RUN_ATTEMPT ?? "1";
const shortSha = (process.env.GITHUB_SHA ?? "sha").slice(0, 7);
if (!pkg.name) {
console.error("Missing package name in package.json");
process.exit(1);
}
if (!match) {
console.error(
`package.json version must match 0.0.x for this release flow. Received: ${pkg.version}`,
);
process.exit(1);
}
const stableVersion = pkg.version;
const nextStableVersion = `0.0.${Number(match[1]) + 1}`;
const prereleaseVersion = `${nextStableVersion}-next.${runNumber}.${runAttempt}.${shortSha}`;
const publishVersion = isBumpPr ? stableVersion : prereleaseVersion;
const publishTag = isBumpPr ? "latest" : "next";
const createBumpPr = isBumpPr ? "false" : "true";
fs.appendFileSync(process.env.GITHUB_OUTPUT, `name=${pkg.name}\n`);
fs.appendFileSync(process.env.GITHUB_OUTPUT, `stable_version=${stableVersion}\n`);
fs.appendFileSync(process.env.GITHUB_OUTPUT, `next_stable_version=${nextStableVersion}\n`);
fs.appendFileSync(process.env.GITHUB_OUTPUT, `publish_version=${publishVersion}\n`);
fs.appendFileSync(process.env.GITHUB_OUTPUT, `publish_tag=${publishTag}\n`);
fs.appendFileSync(process.env.GITHUB_OUTPUT, `create_bump_pr=${createBumpPr}\n`);
NODE
- name: Set package version for prerelease publish
if: steps.meta.outputs.create_bump_pr == 'true'
run: |
node <<'NODE'
const fs = require("node:fs");
const pkg = JSON.parse(fs.readFileSync("package.json", "utf8"));
pkg.version = "${{ steps.meta.outputs.publish_version }}";
fs.writeFileSync("package.json", `${JSON.stringify(pkg, null, 2)}\n`);
NODE
- name: Publish package
id: publish
run: |
PACKAGE_REF="${{ steps.meta.outputs.name }}@${{ steps.meta.outputs.publish_version }}"
if npm view "$PACKAGE_REF" version >/dev/null 2>&1; then
echo "Version $PACKAGE_REF already exists on npm; skipping publish."
echo "published=false" >> "$GITHUB_OUTPUT"
exit 0
fi
npm publish --tag "${{ steps.meta.outputs.publish_tag }}"
echo "published=true" >> "$GITHUB_OUTPUT"
- name: Verify published dist-tag
if: steps.publish.outputs.published == 'true'
run: |
PACKAGE_NAME="${{ steps.meta.outputs.name }}"
PUBLISH_TAG="${{ steps.meta.outputs.publish_tag }}"
EXPECTED_VERSION="${{ steps.meta.outputs.publish_version }}"
for attempt in 1 2 3 4 5 6; do
ACTUAL_VERSION="$(npm view "${PACKAGE_NAME}@${PUBLISH_TAG}" version 2>/dev/null || true)"
if [ "$ACTUAL_VERSION" = "$EXPECTED_VERSION" ]; then
echo "Verified dist-tag ${PUBLISH_TAG}: ${PACKAGE_NAME}@${ACTUAL_VERSION}"
exit 0
fi
if [ "$attempt" -eq 6 ]; then
echo "Dist-tag verification failed for ${PACKAGE_NAME}" >&2
echo "Expected ${PUBLISH_TAG} -> ${EXPECTED_VERSION}, found '${ACTUAL_VERSION}'" >&2
exit 1
fi
sleep 5
done
- name: Verify registry install
if: steps.publish.outputs.published == 'true'
run: |
PACKAGE_REF="${{ steps.meta.outputs.name }}@${{ steps.meta.outputs.publish_version }}"
mkdir -p e2e-install
cd e2e-install
npm init -y
for attempt in 1 2 3 4 5 6; do
if npm install "$PACKAGE_REF"; then
break
fi
if [ "$attempt" -eq 6 ]; then
echo "Failed to install $PACKAGE_REF after retries" >&2
exit 1
fi
sleep 10
done
./node_modules/.bin/sandcode --help
./node_modules/.bin/sandcode analyze --help
./node_modules/.bin/sandcode start --help
./node_modules/.bin/sandcode setup --help
- name: Prepare next patch bump
if: steps.meta.outputs.create_bump_pr == 'true'
run: |
node <<'NODE'
const fs = require("node:fs");
const pkg = JSON.parse(fs.readFileSync("package.json", "utf8"));
pkg.version = "${{ steps.meta.outputs.next_stable_version }}";
fs.writeFileSync("package.json", `${JSON.stringify(pkg, null, 2)}\n`);
NODE
- name: Open bump PR
if: steps.meta.outputs.create_bump_pr == 'true'
uses: peter-evans/create-pull-request@v6
with:
token: ${{ secrets.GH_PAT || secrets.GITHUB_TOKEN }}
branch: ci/version-bump-${{ steps.meta.outputs.next_stable_version }}
draft: true
delete-branch: true
commit-message: "chore: bump package version to ${{ steps.meta.outputs.next_stable_version }}"
title: "chore: bump package version to ${{ steps.meta.outputs.next_stable_version }}"
body: |
Automated post-publish bump.
- Merged PR: #${{ needs.resolve-merge-context.outputs.pr_number }}
- Published `${{ steps.meta.outputs.name }}@${{ steps.meta.outputs.publish_version }}` with npm tag `${{ steps.meta.outputs.publish_tag }}`
- Next stable version prepared: `${{ steps.meta.outputs.next_stable_version }}`
add-paths: |
package.json