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
2 changes: 2 additions & 0 deletions .fernignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,5 @@ changelog.md
.fern/replay.lock
.fern/replay.yml
.gitattributes

.github/scripts/verify-release-commit.sh
80 changes: 80 additions & 0 deletions .github/scripts/validate-release.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -69,3 +69,83 @@ test("package job validates the release before installing or building", () => {
assert.ok(validation < packageJob.indexOf("run: pnpm build"));
assert.ok(validation < packageJob.indexOf("npm pack"));
});

function gitFixture(run) {
const cwd = mkdtempSync(join(tmpdir(), "sdk-release-history-"));
const env = {
...process.env,
GIT_AUTHOR_NAME: "Release test", GIT_AUTHOR_EMAIL: "test@example.com",
GIT_COMMITTER_NAME: "Release test", GIT_COMMITTER_EMAIL: "test@example.com",
};
const git = (...args) => {
const result = spawnSync("git", args, { cwd, env, encoding: "utf8" });
assert.equal(result.status, 0, result.stderr);
return result.stdout.trim();
};
try {
git("init", "--initial-branch=main");
git("commit", "--allow-empty", "-m", "Approved release");
const approved = git("rev-parse", "HEAD");
git("update-ref", "refs/remotes/origin/main", approved);
Object.assign(env, { GITHUB_REF: "refs/tags/v3.0.0", GITHUB_REF_NAME: "v3.0.0", GITHUB_SHA: approved });
const guard = () => spawnSync("bash", [resolve(__dirname, "verify-release-commit.sh")], { cwd, env, encoding: "utf8" });
run({ git, guard, env, approved });
} finally {
rmSync(cwd, { recursive: true, force: true });
}
}

test("allows a release commit on main, including an older approved commit", () => {
gitFixture(({ git, guard, approved }) => {
assert.equal(guard().status, 0);
git("commit", "--allow-empty", "-m", "Newer approved commit");
git("update-ref", "refs/remotes/origin/main", git("rev-parse", "HEAD"));
git("checkout", "--detach", approved);
assert.equal(guard().status, 0);
});
});

test("resolves annotated tag objects to their approved commit", () => {
gitFixture(({ git, guard, env }) => {
git("tag", "-a", "v3.0.0", "-m", "Release");
env.GITHUB_SHA = git("rev-parse", "v3.0.0");
assert.equal(guard().status, 0);
});
});

test("rejects an unmerged release commit", () => {
gitFixture(({ git, guard, env }) => {
git("checkout", "-b", "unapproved");
git("commit", "--allow-empty", "-m", "Unreviewed change");
env.GITHUB_SHA = git("rev-parse", "HEAD");
const result = guard();
assert.equal(result.status, 1);
assert.match(result.stdout, /must already be merged/);
});
});

test("rejects a checkout that differs from the release event", () => {
gitFixture(({ git, guard }) => {
git("commit", "--allow-empty", "-m", "Different checkout");
const result = guard();
assert.equal(result.status, 1);
assert.match(result.stdout, /does not match/);
});
});

test("fails closed when main or the event commit is missing", () => {
gitFixture(({ git, guard, env }) => {
git("update-ref", "-d", "refs/remotes/origin/main");
assert.notEqual(guard().status, 0);
delete env.GITHUB_SHA;
assert.equal(guard().status, 1);
});
});

test("rejects branch refs masquerading as release tags", () => {
gitFixture(({ guard, env }) => {
env.GITHUB_REF = "refs/heads/refs/tags/v3.0.0";
env.GITHUB_REF_NAME = "refs/tags/v3.0.0";
assert.equal(guard().status, 1);
});
});
23 changes: 23 additions & 0 deletions .github/scripts/verify-release-commit.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
#!/usr/bin/env bash
set -euo pipefail

# Fetch origin/main immediately before calling this script. Never release a
# commit that has only appeared on an unreviewed branch or tag.
if [[ -z "${GITHUB_REF_NAME:-}" || "${GITHUB_REF:-}" != "refs/tags/${GITHUB_REF_NAME}" || -z "${GITHUB_SHA:-}" ]]; then
echo "::error::Release requires a tag push and an event commit."
exit 1
fi

release_commit=$(git rev-parse --verify "${GITHUB_SHA}^{commit}")
checkout_commit=$(git rev-parse --verify 'HEAD^{commit}')
if [[ "$checkout_commit" != "$release_commit" ]]; then
echo "::error::Checkout does not match the release event commit."
exit 1
fi

if ! git merge-base --is-ancestor "$release_commit" refs/remotes/origin/main; then
echo "::error::Release commit must already be merged into protected main."
exit 1
fi

echo "Release commit $release_commit is on protected main."
6 changes: 6 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -65,13 +65,19 @@ jobs:
uses: actions/checkout@v6
with:
persist-credentials: false
fetch-depth: 0

- name: Set up node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: "24"
package-manager-cache: false

- name: Verify release commit is on main
run: |
git fetch --no-tags origin +refs/heads/main:refs/remotes/origin/main
bash .github/scripts/verify-release-commit.sh

- name: Validate release tag
run: node .github/scripts/validate-release.cjs

Expand Down
Loading