From 0f3b60ca74ed8301972c37575b3ae434aac19889 Mon Sep 17 00:00:00 2001 From: Ayaz-Microsoft Date: Mon, 29 Jun 2026 17:29:40 +0530 Subject: [PATCH 1/9] fix: replace compromised codfish action with official semantic-release Use npx semantic-release pinned to v22 with conventionalcommits preset 7, configured via .releaserc.json. Removes third-party action attack surface while preserving release behavior. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/create-release.yml | 11 +++++++++ .releaserc.json | 34 ++++++++++++++++++++++++++++ docs/RELEASE_GUIDELINES.md | 10 ++++---- 3 files changed, 50 insertions(+), 5 deletions(-) create mode 100644 .releaserc.json diff --git a/.github/workflows/create-release.yml b/.github/workflows/create-release.yml index 46f145b05..4742b93b6 100644 --- a/.github/workflows/create-release.yml +++ b/.github/workflows/create-release.yml @@ -20,3 +20,14 @@ jobs: uses: actions/checkout@v6 with: ref: ${{ github.event.workflow_run.head_sha }} + fetch-depth: 0 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + + - name: Run semantic-release + run: npx -p semantic-release@22 -p conventional-changelog-conventionalcommits@7 semantic-release + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.releaserc.json b/.releaserc.json new file mode 100644 index 000000000..9955200b1 --- /dev/null +++ b/.releaserc.json @@ -0,0 +1,34 @@ +{ + "branches": ["main"], + "tagFormat": "v${version}", + "plugins": [ + [ + "@semantic-release/commit-analyzer", + { + "preset": "conventionalcommits" + } + ], + [ + "@semantic-release/release-notes-generator", + { + "preset": "conventionalcommits", + "presetConfig": { + "types": [ + { "type": "feat", "section": "Features", "hidden": false }, + { "type": "fix", "section": "Bug Fixes", "hidden": false }, + { "type": "perf", "section": "Performance Improvements", "hidden": false }, + { "type": "revert", "section": "Reverts", "hidden": false }, + { "type": "docs", "section": "Other Updates", "hidden": false }, + { "type": "style", "section": "Other Updates", "hidden": false }, + { "type": "chore", "section": "Other Updates", "hidden": false }, + { "type": "refactor", "section": "Other Updates", "hidden": false }, + { "type": "test", "section": "Other Updates", "hidden": false }, + { "type": "build", "section": "Other Updates", "hidden": false }, + { "type": "ci", "section": "Other Updates", "hidden": false } + ] + } + } + ], + "@semantic-release/github" + ] +} diff --git a/docs/RELEASE_GUIDELINES.md b/docs/RELEASE_GUIDELINES.md index 467949ac8..17a0048a3 100644 --- a/docs/RELEASE_GUIDELINES.md +++ b/docs/RELEASE_GUIDELINES.md @@ -12,21 +12,21 @@ This repository uses GitHub's in-built [Releases](https://docs.github.com/en/rep # Automated releases -In order to automate the generation of a change log, the creation of a release, and the bumping of a version number, we use the [Conventional Changelog Action](https://github.com/TriPSs/conventional-changelog-action). +In order to automate the generation of a change log, the creation of a release, and the bumping of a version number, we use [semantic-release](https://github.com/semantic-release/semantic-release). It works by inferring from the commit history what changes have been made, and hence what version should be assigned. This is why it is important for Pull Request titles to adhere to the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) specification, which many repositories use. This convention uses types such as `docs`, `fix`, `feat`, etc to label commits and PRs. From these, the [semantic version](https://semver.org/) of a release can be identified. For example a release which consists of a PR which adds a feature (`feat`) would result in an increment of the Minor part of the semantic version, e.g. 1.1.0 -> 1.2.0. -Using the Conventional Changelog Action along with GitHub Releases takes all of the manual work out of creating a release. +Using semantic-release along with GitHub Releases takes all of the manual work out of creating a release. -# Conventional Changelog Action +# semantic-release ## Usage -We use the [Conventional Changelog](https://github.com/TriPSs/conventional-changelog-action) GitHub Action, which you can find in `./github/workflows/create-release.yml`. +We use [semantic-release](https://github.com/semantic-release/semantic-release), invoked via `npx` in `./github/workflows/create-release.yml`. Its behaviour is configured in the repository root `.releaserc.json`. -Once a PR is merged to `main`, the Action will automatically run. It will automatically generate a changelog, and if that changelog is empty, then no release is made. This would be the case for merges to `main` that include `docs`, `chore`, etc. +Once the `Validate Deployment` workflow completes successfully on `main`, the Action will automatically run. It will automatically generate a changelog, and if that changelog is empty, then no release is made. This would be the case for merges to `main` that include `docs`, `chore`, etc. Once a merge to `main` is completed that would result in a major/minor/patch version increase (such as `feat`, `fix`, etc.) then a changelog will be generated, and this will trigger a release to be published automatically with the appropriate version number. From 3c449c6f2fc559cea952a49cc666be35cd543228 Mon Sep 17 00:00:00 2001 From: Ayaz-Microsoft Date: Mon, 29 Jun 2026 17:57:53 +0530 Subject: [PATCH 2/9] chore: add temporary test-release workflow for fork validation Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/test-release.yml | 35 ++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 .github/workflows/test-release.yml diff --git a/.github/workflows/test-release.yml b/.github/workflows/test-release.yml new file mode 100644 index 000000000..c9fdddead --- /dev/null +++ b/.github/workflows/test-release.yml @@ -0,0 +1,35 @@ +name: Test-Release (temporary) + +on: + workflow_dispatch: + inputs: + dry_run: + description: "Run in dry-run mode (no release published)" + type: boolean + default: false + +permissions: + contents: write + pull-requests: write + +jobs: + test-release: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + + - name: Run semantic-release + run: | + npx -p semantic-release@22 -p conventional-changelog-conventionalcommits@7 \ + semantic-release --branches "${{ github.ref_name }}" \ + ${{ inputs.dry_run && '--dry-run' || '' }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From e7bc149ccef9d965c33551e24ea3f467f4c01760 Mon Sep 17 00:00:00 2001 From: Ayaz-Microsoft Date: Mon, 29 Jun 2026 18:22:09 +0530 Subject: [PATCH 3/9] chore: remove temporary test-release workflow Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/test-release.yml | 35 ------------------------------ 1 file changed, 35 deletions(-) delete mode 100644 .github/workflows/test-release.yml diff --git a/.github/workflows/test-release.yml b/.github/workflows/test-release.yml deleted file mode 100644 index c9fdddead..000000000 --- a/.github/workflows/test-release.yml +++ /dev/null @@ -1,35 +0,0 @@ -name: Test-Release (temporary) - -on: - workflow_dispatch: - inputs: - dry_run: - description: "Run in dry-run mode (no release published)" - type: boolean - default: false - -permissions: - contents: write - pull-requests: write - -jobs: - test-release: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v6 - with: - fetch-depth: 0 - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: 20 - - - name: Run semantic-release - run: | - npx -p semantic-release@22 -p conventional-changelog-conventionalcommits@7 \ - semantic-release --branches "${{ github.ref_name }}" \ - ${{ inputs.dry_run && '--dry-run' || '' }} - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From 66216214c2cc091ce6b543e31efe1e2f0c3c40a0 Mon Sep 17 00:00:00 2001 From: Ayaz-Microsoft Date: Fri, 17 Jul 2026 17:44:12 +0530 Subject: [PATCH 4/9] feat: implement in-house release script and remove semantic-release configuration --- .github/scripts/release.sh | 254 +++++++++++++++++++++++++++ .github/workflows/create-release.yml | 11 +- .releaserc.json | 34 ---- docs/RELEASE_GUIDELINES.md | 25 ++- 4 files changed, 275 insertions(+), 49 deletions(-) create mode 100644 .github/scripts/release.sh delete mode 100644 .releaserc.json diff --git a/.github/scripts/release.sh b/.github/scripts/release.sh new file mode 100644 index 000000000..df05dc672 --- /dev/null +++ b/.github/scripts/release.sh @@ -0,0 +1,254 @@ +#!/usr/bin/env bash +# +# In-house automated release script. +# +# Replaces third-party release tooling (previously the codfish +# semantic-release-action, then semantic-release). Uses only git, bash, and curl +# against the GitHub REST API, so there are NO external libraries, npm packages, +# or third-party GitHub Actions involved in the release logic. +# +# Behaviour (feature parity with the previous codfish/semantic-release setup): +# * Analyses Conventional Commit subjects since the last vX.Y.Z tag. +# * Bump rules: breaking (! or "BREAKING CHANGE") -> major, feat -> minor, +# fix/perf/revert -> patch. Any other type alone does NOT trigger a release. +# * First ever release is 1.0.0. +# * Release notes group every commit type into sections (Features, Bug Fixes, +# Performance Improvements, Reverts, Other Updates) and include a compare +# link header, autolinked PR/issue references, and autolinked commit SHAs. +# * Tags are formatted as v${version}. The GitHub Release creates the tag. +# * Each released PR/issue gets a "released" label and an inclusion comment. +# * Emits step outputs (new-release-published, release-version) for the job. +# +# Required environment: +# GITHUB_TOKEN - token with contents:write, issues:write, pull-requests:write. +# GITHUB_REPOSITORY - "owner/repo" (provided automatically by Actions). +# +set -euo pipefail + +REPO="${GITHUB_REPOSITORY:?GITHUB_REPOSITORY not set}" +TOKEN="${GITHUB_TOKEN:?GITHUB_TOKEN not set}" +API_URL="${GITHUB_API_URL:-https://api.github.com}" +SERVER_URL="${GITHUB_SERVER_URL:-https://github.com}" +REPO_URL="${SERVER_URL}/${REPO}" +SHA="$(git rev-parse HEAD)" + +# Escape a string so it is safe to embed inside a JSON string literal. +json_escape() { + local s="$1" + s="${s//\\/\\\\}" + s="${s//\"/\\\"}" + s="${s//$'\r'/}" + s="${s//$'\t'/\\t}" + s="${s//$'\n'/\\n}" + printf '%s' "$s" +} + +# Write a key/value pair to the GitHub Actions step output, when available. +set_output() { + if [[ -n "${GITHUB_OUTPUT:-}" ]]; then + printf '%s=%s\n' "$1" "$2" >> "$GITHUB_OUTPUT" + fi +} + +# --- Determine the last released version tag (vMAJOR.MINOR.PATCH) --- +last_tag="$(git tag -l 'v[0-9]*.[0-9]*.[0-9]*' --sort=-v:refname | head -n1 || true)" + +if [[ -n "$last_tag" ]]; then + echo "Last release tag: $last_tag" + range="${last_tag}..HEAD" + base_version="${last_tag#v}" +else + echo "No previous release tag found; this would be the first release." + range="" + base_version="" +fi + +# --- Collect commits in range --- +if [[ -n "$range" ]]; then + raw="$(git log "$range" --no-merges --format='%h%x1f%H%x1f%s%x1f%b%x1e')" +else + raw="$(git log --no-merges --format='%h%x1f%H%x1f%s%x1f%b%x1e')" +fi + +# bump: 0 none, 1 patch, 2 minor, 3 major +bump=0 +feat_lines=() +fix_lines=() +perf_lines=() +revert_lines=() +other_lines=() +breaking_lines=() +declare -A referenced_prs=() + +type_regex='^([a-zA-Z]+)(\(([^)]+)\))?(!)?:[[:space:]]*(.*)$' + +while IFS= read -r -d $'\x1e' record; do + # Skip empty records. + if [[ -z "${record//[$'\n\t ']/}" ]]; then + continue + fi + + short_sha="${record%%$'\x1f'*}" + short_sha="${short_sha//[$'\n\r\t ']/}" + rest="${record#*$'\x1f'}" + full_sha="${rest%%$'\x1f'*}" + full_sha="${full_sha//[$'\n\r\t ']/}" + rest="${rest#*$'\x1f'}" + subject="${rest%%$'\x1f'*}" + body="${rest#*$'\x1f'}" + + if [[ ! "$subject" =~ $type_regex ]]; then + continue + fi + + type="$(printf '%s' "${BASH_REMATCH[1]}" | tr '[:upper:]' '[:lower:]')" + scope="${BASH_REMATCH[3]}" + bang="${BASH_REMATCH[4]}" + desc="${BASH_REMATCH[5]}" + + breaking=0 + if [[ -n "$bang" ]] || printf '%s' "$body" | grep -qE 'BREAKING[ -]CHANGE'; then + breaking=1 + fi + + # Autolink "(#123)" pull request / issue references in the description. + linked_desc="$(printf '%s' "$desc" | sed -E "s@\(#([0-9]+)\)@([#\1](${REPO_URL}/issues/\1))@g")" + commit_link="([${short_sha}](${REPO_URL}/commit/${full_sha}))" + + if [[ -n "$scope" ]]; then + entry="* **${scope}:** ${linked_desc} ${commit_link}" + else + entry="* ${linked_desc} ${commit_link}" + fi + + # Record PR/issue numbers referenced by this commit for later commenting. + while read -r prnum; do + [[ -n "$prnum" ]] && referenced_prs["$prnum"]=1 + done < <(printf '%s' "$subject" | grep -oE '#[0-9]+' | tr -d '#') + + if [[ $breaking -eq 1 ]]; then + breaking_lines+=("$entry") + if (( bump < 3 )); then bump=3; fi + fi + + case "$type" in + feat) feat_lines+=("$entry"); if (( bump < 2 )); then bump=2; fi ;; + fix) fix_lines+=("$entry"); if (( bump < 1 )); then bump=1; fi ;; + perf) perf_lines+=("$entry"); if (( bump < 1 )); then bump=1; fi ;; + revert) revert_lines+=("$entry"); if (( bump < 1 )); then bump=1; fi ;; + docs|style|chore|refactor|test|build|ci) other_lines+=("$entry") ;; + *) : ;; + esac +done <<< "$raw" + +# --- Decide whether a release is warranted --- +if (( bump == 0 )); then + echo "No releasable changes (feat/fix/perf/revert/breaking) found. No release created." + set_output "new-release-published" "false" + exit 0 +fi + +# --- Compute the next version --- +if [[ -z "$base_version" ]]; then + next="1.0.0" +else + IFS='.' read -r MAJOR MINOR PATCH <<< "$base_version" + case $bump in + 3) MAJOR=$((MAJOR + 1)); MINOR=0; PATCH=0 ;; + 2) MINOR=$((MINOR + 1)); PATCH=0 ;; + 1) PATCH=$((PATCH + 1)) ;; + esac + next="${MAJOR}.${MINOR}.${PATCH}" +fi +tag="v${next}" +echo "Next version: $tag" + +# --- Build release notes --- +append_section() { + local title="$1" + local -n arr="$2" + if (( ${#arr[@]} > 0 )); then + notes+="### ${title}"$'\n\n' + local line + for line in "${arr[@]}"; do + notes+="${line}"$'\n' + done + notes+=$'\n' + fi +} + +release_date="$(date -u +%Y-%m-%d)" +if [[ -n "$last_tag" ]]; then + notes="## [${next}](${REPO_URL}/compare/${last_tag}...${tag}) (${release_date})"$'\n\n' +else + notes="## ${next} (${release_date})"$'\n\n' +fi +append_section "⚠ BREAKING CHANGES" breaking_lines +append_section "Features" feat_lines +append_section "Bug Fixes" fix_lines +append_section "Performance Improvements" perf_lines +append_section "Reverts" revert_lines +append_section "Other Updates" other_lines + +echo "----- Release notes -----" +printf '%s\n' "$notes" +echo "-------------------------" + +# --- Create the GitHub Release (also creates the tag at SHA) --- +payload=$(printf '{"tag_name":"%s","target_commitish":"%s","name":"%s","body":"%s","draft":false,"prerelease":false}' \ + "$tag" "$SHA" "$tag" "$(json_escape "$notes")") + +resp_file="$(mktemp)" +http_code=$(curl -sS -o "$resp_file" -w '%{http_code}' -X POST \ + -H "Authorization: Bearer ${TOKEN}" \ + -H "Accept: application/vnd.github+json" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + "${API_URL}/repos/${REPO}/releases" \ + -d "$payload") + +if [[ "$http_code" != "201" ]]; then + echo "Failed to create release (HTTP ${http_code}):" + cat "$resp_file" + rm -f "$resp_file" + exit 1 +fi + +# The release page URL is the first "html_url" in the response body. +release_url="$(grep -oE '"html_url"[[:space:]]*:[[:space:]]*"[^"]*"' "$resp_file" | head -n1 | sed -E 's/.*"html_url"[[:space:]]*:[[:space:]]*"([^"]*)".*/\1/')" +rm -f "$resp_file" +echo "Successfully created release ${tag} (${release_url})." + +set_output "new-release-published" "true" +set_output "release-version" "${next}" + +# --- Comment on and label every released PR/issue (matches @semantic-release/github) --- +comment_body=":tada: This PR is included in version ${next} :tada:"$'\n\n'"The release is available on [GitHub release](${release_url})" +comment_json="$(json_escape "$comment_body")" + +for pr in "${!referenced_prs[@]}"; do + echo "Annotating #${pr}..." + + # Inclusion comment. + c_code=$(curl -sS -o /dev/null -w '%{http_code}' -X POST \ + -H "Authorization: Bearer ${TOKEN}" \ + -H "Accept: application/vnd.github+json" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + "${API_URL}/repos/${REPO}/issues/${pr}/comments" \ + -d "{\"body\":\"${comment_json}\"}") || c_code="000" + if [[ "$c_code" != "201" ]]; then + echo " war: could not comment on #${pr} (HTTP ${c_code})" + fi + + # "released" label. + l_code=$(curl -sS -o /dev/null -w '%{http_code}' -X POST \ + -H "Authorization: Bearer ${TOKEN}" \ + -H "Accept: application/vnd.github+json" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + "${API_URL}/repos/${REPO}/issues/${pr}/labels" \ + -d '{"labels":["released"]}') || l_code="000" + if [[ "$l_code" != "200" ]]; then + echo " war: could not label #${pr} (HTTP ${l_code})" + fi +done + +echo "Release ${tag} complete." diff --git a/.github/workflows/create-release.yml b/.github/workflows/create-release.yml index 4742b93b6..1643a32d9 100644 --- a/.github/workflows/create-release.yml +++ b/.github/workflows/create-release.yml @@ -6,6 +6,7 @@ on: permissions: contents: write + issues: write pull-requests: write name: Create-Release @@ -21,13 +22,9 @@ jobs: with: ref: ${{ github.event.workflow_run.head_sha }} fetch-depth: 0 + fetch-tags: true - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: 20 - - - name: Run semantic-release - run: npx -p semantic-release@22 -p conventional-changelog-conventionalcommits@7 semantic-release + - name: Create release + run: bash .github/scripts/release.sh env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.releaserc.json b/.releaserc.json deleted file mode 100644 index 9955200b1..000000000 --- a/.releaserc.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "branches": ["main"], - "tagFormat": "v${version}", - "plugins": [ - [ - "@semantic-release/commit-analyzer", - { - "preset": "conventionalcommits" - } - ], - [ - "@semantic-release/release-notes-generator", - { - "preset": "conventionalcommits", - "presetConfig": { - "types": [ - { "type": "feat", "section": "Features", "hidden": false }, - { "type": "fix", "section": "Bug Fixes", "hidden": false }, - { "type": "perf", "section": "Performance Improvements", "hidden": false }, - { "type": "revert", "section": "Reverts", "hidden": false }, - { "type": "docs", "section": "Other Updates", "hidden": false }, - { "type": "style", "section": "Other Updates", "hidden": false }, - { "type": "chore", "section": "Other Updates", "hidden": false }, - { "type": "refactor", "section": "Other Updates", "hidden": false }, - { "type": "test", "section": "Other Updates", "hidden": false }, - { "type": "build", "section": "Other Updates", "hidden": false }, - { "type": "ci", "section": "Other Updates", "hidden": false } - ] - } - } - ], - "@semantic-release/github" - ] -} diff --git a/docs/RELEASE_GUIDELINES.md b/docs/RELEASE_GUIDELINES.md index 17a0048a3..2fa65bad8 100644 --- a/docs/RELEASE_GUIDELINES.md +++ b/docs/RELEASE_GUIDELINES.md @@ -12,31 +12,40 @@ This repository uses GitHub's in-built [Releases](https://docs.github.com/en/rep # Automated releases -In order to automate the generation of a change log, the creation of a release, and the bumping of a version number, we use [semantic-release](https://github.com/semantic-release/semantic-release). +In order to automate the generation of a change log, the creation of a release, and the bumping of a version number, we use an in-house release script (`./.github/scripts/release.sh`) that relies only on `git`, `bash`, and `curl` against the GitHub REST API. There are no external libraries, npm packages, or third-party GitHub Actions involved in the release logic. It works by inferring from the commit history what changes have been made, and hence what version should be assigned. This is why it is important for Pull Request titles to adhere to the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) specification, which many repositories use. This convention uses types such as `docs`, `fix`, `feat`, etc to label commits and PRs. From these, the [semantic version](https://semver.org/) of a release can be identified. For example a release which consists of a PR which adds a feature (`feat`) would result in an increment of the Minor part of the semantic version, e.g. 1.1.0 -> 1.2.0. -Using semantic-release along with GitHub Releases takes all of the manual work out of creating a release. +Using the in-house release script along with GitHub Releases takes all of the manual work out of creating a release. -# semantic-release +# In-house release script ## Usage -We use [semantic-release](https://github.com/semantic-release/semantic-release), invoked via `npx` in `./github/workflows/create-release.yml`. Its behaviour is configured in the repository root `.releaserc.json`. +We use the release script at `./.github/scripts/release.sh`, invoked from `./.github/workflows/create-release.yml`. All of its behaviour lives in that single script — no configuration file or external tooling is required. -Once the `Validate Deployment` workflow completes successfully on `main`, the Action will automatically run. It will automatically generate a changelog, and if that changelog is empty, then no release is made. This would be the case for merges to `main` that include `docs`, `chore`, etc. +Once the `Validate Deployment` workflow completes successfully on `main`, the workflow will automatically run the script. It analyses the commits since the last `vX.Y.Z` tag, generates a changelog, and if there are no releasable changes then no release is made. This would be the case for merges to `main` that only include `docs`, `chore`, etc. Once a merge to `main` is completed that would result in a major/minor/patch version increase (such as `feat`, `fix`, etc.) then a changelog will be generated, and this will trigger a release to be published automatically with the appropriate version number. -By default, `semantic-release` only includes `fix`, `feat`, and `perf` commit types in the release. Our project includes all commit types in their release notes, while still using `semantic-release`'s commit analyzer to only create releases for `fix`, `feat`, and `perf` commits. +The script includes **all** commit types in the release notes (grouping `docs`, `style`, `chore`, `refactor`, `test`, `build`, and `ci` under "Other Updates"), while still only creating releases for `feat`, `fix`, `perf`, `revert`, and breaking changes. The exact bump rules are: -Note that, it is not possible to automate the update of a `CHANGELOG` as this would require the GitHub token to have permissions to push commits to the repository, which cannot be enabled. + * breaking change (`!` suffix or a `BREAKING CHANGE` note) → **major** + * `feat` → **minor** + * `fix`, `perf`, `revert` → **patch** + * any other type alone → **no release** + +The first ever release (when no `vX.Y.Z` tag exists yet) is published as `1.0.0`. + +For each released Pull Request (and referenced issue), the script also adds a `released` label and posts a comment noting which version it shipped in — matching the previous behaviour of `@semantic-release/github`. The release notes include a compare link, autolinked PR/issue references, and autolinked commit SHAs. + +Note that, it is not possible to automate the update of a `CHANGELOG` file as this would require the GitHub token to have permissions to push commits to the repository, which cannot be enabled. The generated changelog therefore lives on the GitHub Release only. ## Security -The GitHub Action to create the release requires only the GitHub token, as this has sufficient permissions for it to checkout main (and read the commit history) and to create a release for the repository. +The release script requires only the GitHub token, as this has sufficient permissions for it to checkout `main` (and read the commit history) and to create a release for the repository. Because the release logic is entirely in-house (`git` + `curl` + the GitHub REST API), there is no third-party action or package in the release path to trust or keep patched. # Conventional Commits From eb7ff8293ecfd98065f5dfa5ca80f1fc720b8a1a Mon Sep 17 00:00:00 2001 From: Ayaz-Microsoft Date: Fri, 17 Jul 2026 17:52:50 +0530 Subject: [PATCH 5/9] chore: add temporary test-release workflow and dry-run mode for validation Adds a manually-triggered (workflow_dispatch) Test-Release workflow and a guarded DRY_RUN mode in release.sh so the in-house release script can be validated on a fork without publishing a release. Remove the temporary workflow after validation. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 932a7437-8fa0-469e-9faf-045fab35f3a0 --- .github/scripts/release.sh | 11 ++++++++++ .github/workflows/test-release.yml | 35 ++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+) create mode 100644 .github/workflows/test-release.yml diff --git a/.github/scripts/release.sh b/.github/scripts/release.sh index df05dc672..6b4b4a6cc 100644 --- a/.github/scripts/release.sh +++ b/.github/scripts/release.sh @@ -194,6 +194,17 @@ echo "----- Release notes -----" printf '%s\n' "$notes" echo "-------------------------" +# --- Dry-run: report what would happen without publishing anything --- +if [[ "${DRY_RUN:-false}" == "true" ]]; then + echo "DRY RUN: would create release ${tag} at ${SHA}." + if (( ${#referenced_prs[@]} > 0 )); then + echo "DRY RUN: would add the 'released' label and an inclusion comment to: ${!referenced_prs[*]}" + fi + set_output "new-release-published" "true" + set_output "release-version" "${next}" + exit 0 +fi + # --- Create the GitHub Release (also creates the tag at SHA) --- payload=$(printf '{"tag_name":"%s","target_commitish":"%s","name":"%s","body":"%s","draft":false,"prerelease":false}' \ "$tag" "$SHA" "$tag" "$(json_escape "$notes")") diff --git a/.github/workflows/test-release.yml b/.github/workflows/test-release.yml new file mode 100644 index 000000000..76ab14e7f --- /dev/null +++ b/.github/workflows/test-release.yml @@ -0,0 +1,35 @@ +name: Test-Release (temporary) + +# Temporary workflow for manually validating the in-house release script +# (.github/scripts/release.sh) on a fork, without waiting for the +# "Validate Deployment" workflow. Runs in dry-run mode by default so nothing +# is published. Remove this file once validation is complete. + +on: + workflow_dispatch: + inputs: + dry_run: + description: "Run in dry-run mode (no release published, no PRs annotated)" + type: boolean + default: true + +permissions: + contents: write + issues: write + pull-requests: write + +jobs: + test-release: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + fetch-depth: 0 + fetch-tags: true + + - name: Run in-house release script + run: bash .github/scripts/release.sh + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + DRY_RUN: ${{ inputs.dry_run }} From 3d4d99a8323eec021d5c2b07af7790c8c396e1f3 Mon Sep 17 00:00:00 2001 From: Ayaz-Microsoft Date: Fri, 17 Jul 2026 17:55:47 +0530 Subject: [PATCH 6/9] chore: remove temporary test-release workflow Validation complete; removing the temporary workflow_dispatch workflow. The guarded DRY_RUN mode remains in release.sh for future manual checks. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 932a7437-8fa0-469e-9faf-045fab35f3a0 --- .github/workflows/test-release.yml | 35 ------------------------------ 1 file changed, 35 deletions(-) delete mode 100644 .github/workflows/test-release.yml diff --git a/.github/workflows/test-release.yml b/.github/workflows/test-release.yml deleted file mode 100644 index 76ab14e7f..000000000 --- a/.github/workflows/test-release.yml +++ /dev/null @@ -1,35 +0,0 @@ -name: Test-Release (temporary) - -# Temporary workflow for manually validating the in-house release script -# (.github/scripts/release.sh) on a fork, without waiting for the -# "Validate Deployment" workflow. Runs in dry-run mode by default so nothing -# is published. Remove this file once validation is complete. - -on: - workflow_dispatch: - inputs: - dry_run: - description: "Run in dry-run mode (no release published, no PRs annotated)" - type: boolean - default: true - -permissions: - contents: write - issues: write - pull-requests: write - -jobs: - test-release: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v6 - with: - fetch-depth: 0 - fetch-tags: true - - - name: Run in-house release script - run: bash .github/scripts/release.sh - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - DRY_RUN: ${{ inputs.dry_run }} From 44b289ff7c841168d5620dd12ad98c260b8b8e67 Mon Sep 17 00:00:00 2001 From: Ayaz-Microsoft Date: Tue, 21 Jul 2026 14:14:08 +0530 Subject: [PATCH 7/9] Re-add temporary Test-Release workflow for release script validation Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 60db667d-ec84-4390-a8bb-dd0e790680a4 --- .github/workflows/test-release.yml | 35 ++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 .github/workflows/test-release.yml diff --git a/.github/workflows/test-release.yml b/.github/workflows/test-release.yml new file mode 100644 index 000000000..76ab14e7f --- /dev/null +++ b/.github/workflows/test-release.yml @@ -0,0 +1,35 @@ +name: Test-Release (temporary) + +# Temporary workflow for manually validating the in-house release script +# (.github/scripts/release.sh) on a fork, without waiting for the +# "Validate Deployment" workflow. Runs in dry-run mode by default so nothing +# is published. Remove this file once validation is complete. + +on: + workflow_dispatch: + inputs: + dry_run: + description: "Run in dry-run mode (no release published, no PRs annotated)" + type: boolean + default: true + +permissions: + contents: write + issues: write + pull-requests: write + +jobs: + test-release: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + fetch-depth: 0 + fetch-tags: true + + - name: Run in-house release script + run: bash .github/scripts/release.sh + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + DRY_RUN: ${{ inputs.dry_run }} From 0a04988d35f2a79f10ec14b7bc0e7308fba6d20d Mon Sep 17 00:00:00 2001 From: Ayaz-Microsoft Date: Tue, 21 Jul 2026 14:14:52 +0530 Subject: [PATCH 8/9] Remove temporary Test-Release workflow after release script validation Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 60db667d-ec84-4390-a8bb-dd0e790680a4 --- .github/workflows/test-release.yml | 35 ------------------------------ 1 file changed, 35 deletions(-) delete mode 100644 .github/workflows/test-release.yml diff --git a/.github/workflows/test-release.yml b/.github/workflows/test-release.yml deleted file mode 100644 index 76ab14e7f..000000000 --- a/.github/workflows/test-release.yml +++ /dev/null @@ -1,35 +0,0 @@ -name: Test-Release (temporary) - -# Temporary workflow for manually validating the in-house release script -# (.github/scripts/release.sh) on a fork, without waiting for the -# "Validate Deployment" workflow. Runs in dry-run mode by default so nothing -# is published. Remove this file once validation is complete. - -on: - workflow_dispatch: - inputs: - dry_run: - description: "Run in dry-run mode (no release published, no PRs annotated)" - type: boolean - default: true - -permissions: - contents: write - issues: write - pull-requests: write - -jobs: - test-release: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v6 - with: - fetch-depth: 0 - fetch-tags: true - - - name: Run in-house release script - run: bash .github/scripts/release.sh - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - DRY_RUN: ${{ inputs.dry_run }} From a9675f8724530f2014d423c7b91a9c8a52791b0e Mon Sep 17 00:00:00 2001 From: Ayaz-Microsoft Date: Thu, 23 Jul 2026 11:18:13 +0530 Subject: [PATCH 9/9] ci: harden create-release (trust guard, drop pull-requests:write, SHA-pin checkout) --- .github/workflows/create-release.yml | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/.github/workflows/create-release.yml b/.github/workflows/create-release.yml index 1643a32d9..403310121 100644 --- a/.github/workflows/create-release.yml +++ b/.github/workflows/create-release.yml @@ -7,18 +7,26 @@ on: permissions: contents: write issues: write - pull-requests: write name: Create-Release jobs: create-release: runs-on: ubuntu-latest - if: ${{ github.event.workflow_run.conclusion == 'success' }} + # Trust guard: only run for successful runs triggered by a non-fork, + # non-pull_request event on this same repository. This prevents the + # privileged workflow_run context (write-scoped GITHUB_TOKEN + secrets) + # from ever checking out and executing attacker-controlled code from a + # fork PR head (CodeQL: actions/untrusted-checkout). + if: >- + ${{ github.event.workflow_run.conclusion == 'success' && + github.event.workflow_run.event != 'pull_request' && + github.event.workflow_run.head_repository.full_name == github.repository }} steps: - name: Checkout - uses: actions/checkout@v6 + # SHA-pinned to actions/checkout v6 (d23441a) for supply-chain safety. + uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 with: ref: ${{ github.event.workflow_run.head_sha }} fetch-depth: 0