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
20 changes: 18 additions & 2 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ on:
required: false
type: string

concurrency: ${{ github.workflow }}-${{ github.ref }}-${{ inputs.version || inputs.bump }}
# One release at a time. Keying the group on the inputs let a `patch` and a
# `minor` dispatch run concurrently and race on tags and the GitHub release.
concurrency: ${{ github.workflow }}

permissions:
id-token: write
Expand All @@ -28,6 +30,12 @@ jobs:
runs-on: ubuntu-latest
if: github.repository == 'synthetic-sciences/OpenScience'
steps:
- name: Require a bump or version input
if: ${{ !inputs.bump && !inputs.version }}
run: |
echo "::error::Provide 'bump' or 'version'. An empty dispatch would publish a 0.0.0-main preview to npm as if it were a release."
exit 1

- uses: actions/checkout@v4
with:
fetch-depth: 0
Expand Down Expand Up @@ -107,6 +115,9 @@ jobs:
OPENSCIENCE_VERSION: ${{ needs.version.outputs.version }}
OPENSCIENCE_RELEASE: ${{ needs.version.outputs.release }}
GITHUB_TOKEN: ${{ github.token }}
# Fine-grained PAT with push access to synthetic-sciences/homebrew-tap;
# github.token is repo-scoped and can't update the tap.
HOMEBREW_TAP_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }}
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
NPM_CONFIG_PROVENANCE: "true"

Expand All @@ -116,19 +127,24 @@ jobs:
needs:
- version
- publish
# Previews (no release output) shouldn't create a production deployment.
if: needs.version.outputs.release != ''
runs-on: ubuntu-latest
permissions:
deployments: write
steps:
- uses: actions/github-script@v8
env:
VERSION: ${{ needs.version.outputs.version }}
TAG: ${{ needs.version.outputs.tag }}
with:
script: |
const version = process.env.VERSION
const deployment = await github.rest.repos.createDeployment({
...context.repo,
ref: context.sha,
// The tag points at the release commit (the tree that was
// actually published); context.sha is the pre-release commit.
ref: process.env.TAG || context.sha,
environment: "npm",
auto_merge: false,
required_contexts: [],
Expand Down
11 changes: 7 additions & 4 deletions backend/cli/script/publish.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,11 +160,13 @@ if (!Script.preview) {
"",
].join("\n")

const token = process.env.GITHUB_TOKEN
// The workflow's github.token is scoped to this repo and can never push
// to the tap repo — a dedicated fine-grained PAT is required.
const token = process.env.HOMEBREW_TAP_TOKEN
if (!token) {
console.warn("GITHUB_TOKEN not set, skipping homebrew tap update")
console.warn("::warning title=homebrew skipped::HOMEBREW_TAP_TOKEN not set — brew users stay on the previous version")
} else {
const tap = `https://x-access-token:${token}@github.com/syntheticsciences/homebrew-tap.git`
const tap = `https://x-access-token:${token}@github.com/synthetic-sciences/homebrew-tap.git`
await $`rm -rf ./dist/homebrew-tap`
await $`git clone ${tap} ./dist/homebrew-tap`
await Bun.file("./dist/homebrew-tap/openscience.rb").write(homebrewFormula)
Expand All @@ -173,6 +175,7 @@ if (!Script.preview) {
await $`cd ./dist/homebrew-tap && git push`
}
} catch (e) {
console.warn("Registry update (Homebrew/AUR) failed, skipping:", e)
const message = e instanceof Error ? e.message : String(e)
console.warn(`::warning title=homebrew failed::tap update failed — brew users stay on the previous version (${message})`)
}
}
46 changes: 36 additions & 10 deletions tooling/repo/publish.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,11 +52,25 @@ await import(`../sdk/js/script/build.ts`)
if (Script.release) {
await $`git commit -am "release: v${Script.version}"`.nothrow()
await $`git tag v${Script.version}`.nothrow()
await $`git fetch origin`.nothrow()
await $`git cherry-pick HEAD..origin/dev`.nothrow()
await $`git push origin HEAD --tags --no-verify --force-with-lease`.nothrow()
await new Promise((resolve) => setTimeout(resolve, 5_000))
await $`gh release edit v${Script.version} --draft=false`.nothrow()
// Tags are exempt from branch protection; push the tag on its own so the
// release assets and npm publishes can proceed regardless of what happens
// to the branch push below.
const tagPush = await $`git push origin refs/tags/v${Script.version} --no-verify`.nothrow()
if (tagPush.exitCode !== 0) {
console.warn(`::warning::tag push failed for v${Script.version} (already pushed?)`)
}
// Branch protection rejects direct pushes to main, and the old .nothrow()
// swallowed that — every release left its version-bump commit orphaned and
// main's package versions permanently stale. Try the direct push (works if
// the workflow identity is a bypass actor), otherwise open a release PR so
// the bumps land through the normal checks.
const push = await $`git push origin HEAD:main --no-verify`.nothrow()
if (push.exitCode !== 0) {
const branch = `release/v${Script.version}`
console.warn(`main push rejected by branch protection — opening a release PR from ${branch}`)
await $`git push origin HEAD:refs/heads/${branch} --force --no-verify`
await $`gh pr create --base main --head ${branch} --title "release: v${Script.version}" --body "Version bumps from the v${Script.version} release."`.nothrow()
}
}

// Sections keep publishing past an earlier failure so a broken CLI publish
Expand Down Expand Up @@ -122,14 +136,18 @@ try {
// chase (`npm owner add <token-user> synsci`), not a broken release —
// every other package shipped, so warn loudly instead of failing.
if (stderr.includes("E403") || stderr.includes("do not have permission")) {
// A GitHub Actions annotation, so this shows on the run summary
// instead of being a log line nobody reads on a green run.
console.warn(
"launcher NOT published: the npm token's account is not an owner of the 'synsci' package.\n" +
"Fix: an owner runs `npm owner add <token-user> synsci`, then re-release.",
"::warning title=launcher not published::npm token's account is not an owner of the 'synsci' package — users keep getting the previous launcher. Fix: an owner runs `npm owner add <token-user> synsci`, then re-release.",
)
} else if (stderr.includes("cannot publish over") || stderr.includes("previously published")) {
// The launcher sometimes ships out-of-band between releases; finding
// this version already on the registry is success, not failure.
console.warn(`launcher ${Script.version} already on the registry, skipping`)
// The launcher sometimes ships out-of-band between releases. That
// means the release-built launcher for this version will never ship,
// so surface it on the run summary rather than silently skipping.
console.warn(
`::warning title=launcher skipped::synsci@${Script.version} already exists on the registry (published out-of-band) — the release-built launcher was NOT published.`,
)
} else {
throw new Error(`npm publish exited with ${result.exitCode}`)
}
Expand All @@ -144,5 +162,13 @@ process.chdir(dir)

if (failures.length > 0) {
console.error(`\npublish failed for: ${failures.join(", ")}`)
if (Script.release) console.error("release left as draft so releases/latest doesn't move")
process.exit(1)
}

if (Script.release) {
// Undraft last. The install script resolves releases/latest, so going
// public before npm has the packages would leave curl installs ahead of
// npm installs whenever a publish fails.
await $`gh release edit v${Script.version} --draft=false`
}
Loading