Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 97 additions & 0 deletions .github/scripts/open-release-pr.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
#!/usr/bin/env node
// Open a release PR and enable auto-merge — without the gh CLI, which is not
// installed on the self-hosted runner. Uses the REST API to create the PR and the GraphQL
// API to enable auto-merge. Relies only on Node's global fetch (Node >= 22).
//
// The PR targets BASE_BRANCH (default: develop) — the version bump lands on develop
// first and reaches main through the normal develop -> main promotion, never by a
// direct push or PR to main.
//
// Required env: GH_TOKEN, GITHUB_REPOSITORY (owner/repo), VERSION, RELEASE_BRANCH.
// Optional env: BASE_BRANCH (default "develop").

const token = process.env.GH_TOKEN;
const repo = process.env.GITHUB_REPOSITORY;
const version = process.env.VERSION;
const branch = process.env.RELEASE_BRANCH;
const baseBranch = process.env.BASE_BRANCH || 'develop';

for (const [k, v] of Object.entries({ GH_TOKEN: token, GITHUB_REPOSITORY: repo, VERSION: version, RELEASE_BRANCH: branch })) {
if (!v) {
console.error(`::error::Missing required env ${k}`);
process.exit(1);
}
}

const [owner, name] = repo.split('/');

async function rest(path, init = {}) {
const res = await fetch(`https://api.github.com${path}`, {
...init,
headers: {
Authorization: `Bearer ${token}`,
Accept: 'application/vnd.github+json',
'X-GitHub-Api-Version': '2022-11-28',
'Content-Type': 'application/json',
...(init.headers ?? {}),
},
});
const body = await res.json().catch(() => ({}));
return { status: res.status, body };
}

async function graphql(query, variables) {
const res = await fetch('https://api.github.com/graphql', {
method: 'POST',
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ query, variables }),
});
return res.json();
}

const prBody =
`Automated version bump for v${version}. Tag \`v${version}\` and the npm publish already ` +
`completed; merging this lands the bumped package.json on \`${baseBranch}\`, from where it ` +
`is promoted to main through the regular ${baseBranch} -> main merge.`;

// Create the PR — or reuse an existing one for this branch (idempotent on re-run).
let { status, body } = await rest(`/repos/${owner}/${name}/pulls`, {
method: 'POST',
body: JSON.stringify({ title: `release: v${version}`, head: branch, base: baseBranch, body: prBody }),
});

let pr;
if (status === 201) {
pr = body;
} else {
// 422 typically means a PR already exists for this head — find and reuse it.
const existing = await rest(`/repos/${owner}/${name}/pulls?head=${owner}:${branch}&state=open`);
if (existing.status === 200 && Array.isArray(existing.body) && existing.body.length > 0) {
pr = existing.body[0];
console.log(`Reusing existing PR #${pr.number}`);
} else {
console.error(`::error::Failed to create PR (HTTP ${status}): ${JSON.stringify(body)}`);
process.exit(1);
}
}

console.log(`PR #${pr.number}: ${pr.html_url}`);

// Enable auto-merge (squash) so it lands once required checks/approvals pass.
const result = await graphql(
`mutation($id: ID!) {
enablePullRequestAutoMerge(input: { pullRequestId: $id, mergeMethod: SQUASH }) {
pullRequest { number }
}
}`,
{ id: pr.node_id },
);

if (result.errors?.length) {
console.log(
`::warning::Could not enable auto-merge for PR #${pr.number}: ${result.errors.map((e) => e.message).join('; ')}. ` +
`Ensure "Allow auto-merge" is enabled in repo settings. The PR is open for manual merge.`,
);
} else {
console.log(`Auto-merge (squash) enabled for PR #${pr.number}.`);
}
16 changes: 14 additions & 2 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,11 @@ jobs:
git config user.email "github-actions[bot]@users.noreply.github.com"
npm version ${{ inputs.version }} --no-git-tag-version
VERSION=$(node -p "require('./package.json').version")
echo "VERSION=${VERSION}" >> "$GITHUB_ENV"
echo "RELEASE_BRANCH=release/v${VERSION}" >> "$GITHUB_ENV"
# Never touch main directly: commit the bump to a release branch; a PR
# into develop is opened below, and develop is promoted to main manually.
git switch -c "release/v${VERSION}"
git add package.json
git commit -m "release: v${VERSION}"
git tag "v${VERSION}"
Expand All @@ -66,8 +71,15 @@ jobs:
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

- name: Push version commit and tag
- name: Push release branch and tag, open auto-merge PR into develop
if: ${{ inputs.dry-run != true }}
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
BASE_BRANCH: develop
run: |
git push origin main
git push origin "${RELEASE_BRANCH}"
git push origin --tags
# gh CLI is not installed on the self-hosted runner — open the PR + enable
# auto-merge via the GitHub API (Node global fetch) instead. The PR targets
# develop; main only receives the bump via the manual develop -> main merge.
node .github/scripts/open-release-pr.mjs
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "devglide",
"version": "0.2.22",
"version": "0.2.35",
"description": "AI workflow toolkit with MCP servers for kanban, shell, testing, workflows, and more — built for Claude Code",
"license": "MIT",
"author": "Daniel Kutyla",
Expand Down
Loading