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
92 changes: 77 additions & 15 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,50 +23,112 @@ jobs:
# release path. The author and head repo clauses are what make it unforgeable; the branch
# name on its own would let any PR, a fork's included, call its branch release-please--x.
# Label a Release PR `run-tests` to force the lane anyway.
#
# Keep this job id: it prefixes every check name the reusable workflow produces.
ci:
# The skip also requires the diff to be only what release-please writes: the changelog, the manifest, and in each version file nothing but the version line. A hand-pushed code or dependency change, an unexpected file or a failed lookup runs the unit lane.
release-only:
if: >-
${{ contains(github.event.pull_request.labels.*.name, 'run-tests')
|| !(github.event.pull_request.user.login == 'github-actions[bot]'
${{ github.event.pull_request.user.login == 'github-actions[bot]'
&& github.event.pull_request.head.repo.full_name == github.repository
&& startsWith(github.head_ref, 'release-please--')) }}
&& startsWith(github.head_ref, 'release-please--') }}
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
contents: read
pull-requests: read
outputs:
skip: ${{ steps.files.outputs.skip }}
steps:
- id: files
env:
GH_TOKEN: ${{ github.token }}
PR: ${{ github.event.pull_request.number }}
VERSION_FILES: version.go
run: |
export FILES="$RUNNER_TEMP/pr-files.jsonl"
if ! gh api "repos/${GITHUB_REPOSITORY}/pulls/${PR}/files" --paginate --jq '.[] | @json' > "$FILES"; then
echo "::warning::Could not list this PR's files; running the unit lane."
echo "skip=false" >> "$GITHUB_OUTPUT"
exit 0
fi
python3 - <<'PY'
import json, os, re

files = [json.loads(line) for line in open(os.environ["FILES"]) if line.strip()]
version_files = set(os.environ["VERSION_FILES"].split())
free = {"CHANGELOG.md", ".release-please-manifest.json"}
version_token = re.compile(r"v?\d+(?:\.\d+)+(?:-[0-9A-Za-z.]+)?")


def lines(f, sign):
return [l[1:] for l in (f.get("patch") or "").split("\n") if l.startswith(sign)]


def decide():
manifest = next((f for f in files if f["filename"] == ".release-please-manifest.json"), None)
new = re.findall(r'"\.":\s*"([^"]+)"', "\n".join(lines(manifest, "+"))) if manifest else []
if len(new) != 1:
return "the manifest diff is not a single version bump"
has_new = re.compile(r"(?<![\w.])v?" + re.escape(new[0]) + r"(?![\w.])")
for f in files:
name = f["filename"]
if name in free:
continue
if name not in version_files:
return f"{name} is not a file release-please writes"
if f.get("patch") is None:
return f"GitHub returned no diff for {name}"
added, removed = lines(f, "+"), lines(f, "-")
stray = next((l for l in added if not has_new.search(l)), None)
if stray is not None:
return f"{name} adds a line without the new version: {stray.strip()[:120]}"
# Masking versions, what was removed must be exactly what was added back.
if sorted(version_token.sub("V", l) for l in removed) != sorted(version_token.sub("V", l) for l in added):
return f"{name} changes more than its version line"
return None


reason = decide()
with open(os.environ.get("GITHUB_OUTPUT", "/dev/stdout"), "a") as out:
out.write(f"skip={'false' if reason else 'true'}\n")
if reason:
print(f"::notice::Running the unit lane: {reason}")
PY

# Keep this job id: it prefixes every check name the reusable workflow produces.
ci:
needs: release-only
if: ${{ !cancelled() && (contains(github.event.pull_request.labels.*.name, 'run-tests') || needs.release-only.outputs.skip != 'true') }}
uses: ./.github/workflows/run_tests.yml
secrets:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}

# The one required status check on main. A matrix job skipped by `if:` publishes a single
# check run with the template unexpanded, so per-leg contexts could never be satisfied on a
# Release PR; this job carries no matrix for that reason. `skipped` is accepted only on a
# Release PR, repeated here because Actions cannot share an expression. `!cancelled()`
# Release PR; this job carries no matrix for that reason. `skipped` is accepted only when release-only confirmed a release-please-only diff. `!cancelled()`
# rather than `always()`: a cancelled run must stay red, not report a pass. Only a real run
# of this workflow can republish it, which is why the run-tests escape hatch lives in
# label_tests.yml: a label event reaching here would republish the context having tested
# nothing, turning a red gate green.
tests-passed:
name: 🧪 Tests
needs: ci
needs: [release-only, ci]
if: ${{ !cancelled() }}
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Check the unit lane
env:
RESULT: ${{ needs.ci.result }}
RELEASE_PR: >-
${{ github.event.pull_request.user.login == 'github-actions[bot]'
&& github.event.pull_request.head.repo.full_name == github.repository
&& startsWith(github.head_ref, 'release-please--') }}
SKIP: ${{ needs.release-only.outputs.skip }}
run: |
case "$RESULT" in
success)
echo "unit lane passed"
;;
skipped)
if [ "$RELEASE_PR" = "true" ]; then
if [ "$SKIP" = "true" ]; then
echo "release pr: unit lane skipped by design"
else
echo "::error::the unit lane was skipped on a PR that is not a Release PR"
echo "::error::the unit lane was skipped without release-only confirming a release-please-only diff"
exit 1
fi
;;
Expand Down
17 changes: 7 additions & 10 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,7 @@ jobs:
target-branch: ${{ github.ref_name }}
skip-github-release: true

# The tag is irreversible and, for Go, permanent: proxy.golang.org caches it forever.
# So the suite has to run before it, which means knowing a release is pending before
# the suite starts. The tag lands on the merged Release PR's merge commit while the
# suite runs on this workflow's own commit, so `ready` also requires those to be the
# same commit. They are, on the path that matters: the push of that merge.
# The tag is irreversible and, for Go, permanent: proxy.golang.org caches it forever, so they run only on the push that merged the Release PR: `ready` requires the pending release's merge commit to be this run's commit. A release from the default branch has no test run, because the Release PR adds only the version bump and changelog to already-tested code; a hotfix release from `N.x` runs the unit lane first, because hotfix commits are pushed without a PR.
detect:
name: Detect pending release
if: github.ref_name == 'main' || endsWith(github.ref_name, '.x')
Comment thread
mogita marked this conversation as resolved.
Expand Down Expand Up @@ -117,8 +113,7 @@ jobs:
echo "No pending release on ${BASE}."
elif [ "$sha" != "$HEAD_SHA" ]; then
# Reached when an earlier release run failed after the Release PR merged.
# Tagging $sha here would tag a tree this run never tested, and failing
# would redden every later push, so stand down and say why.
# Finishing it from a later push would retry a deterministic failure (a refused major tag, a broken build) on every push, so stand down and say why.
pending=true
echo "::warning::Release PR #${num} is still pending at ${sha}, which is not this run's commit ${HEAD_SHA}. Re-run the workflow run for ${sha} to finish that release."
{
Expand Down Expand Up @@ -179,9 +174,9 @@ jobs:
echo "Release $version will be tagged at $SHA, where go.mod is $module_path."

tests:
name: Tests
name: Tests (hotfix only)
needs: detect
if: needs.detect.outputs.ready == 'true'
if: needs.detect.outputs.ready == 'true' && github.ref_name != github.event.repository.default_branch
uses: ./.github/workflows/run_tests.yml
secrets:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
Expand All @@ -190,7 +185,9 @@ jobs:
release:
name: 🚀 Tag and release
needs: [detect, tests]
if: needs.detect.outputs.ready == 'true'
if: >-
${{ !cancelled() && needs.detect.result == 'success' && needs.detect.outputs.ready == 'true'
&& (needs.tests.result == 'success' || needs.tests.result == 'skipped') }}
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
Expand Down
6 changes: 3 additions & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ CI follows the same split:
| --- | --- | --- |
| Pull request | `go test -short` on Go 1.19 to 1.24 | yes, `🧪 Tests` |
| Daily at 14:00 UTC | the full suite on Go 1.24 | no, a red run opens an issue |
| Push to `main` with a release pending | the unit lane | yes, it gates the tag |
| Release PR merged | nothing on the default branch, the unit lane on `N.x` | `N.x` only |

The full suite requires at least two environment variables: `STREAM_API_KEY` and `STREAM_API_SECRET`. There are multiple ways to provide that:
- simply set it in your current shell (`export STREAM_API_KEY=xyz`)
Expand Down Expand Up @@ -62,8 +62,8 @@ Releases are driven by [release-please](https://github.com/googleapis/release-pl
- Merge PRs to `main` with conventional-commit titles. The PR title becomes the commit subject and is what determines the next version, so a non-conventional title ships nothing.
- release-please keeps a Release PR open with the version bump and the generated changelog. Review it.
- The Release PR is opened by `github-actions[bot]`, so its CI runs are held at "action required". If the PR is behind `main`, clicking **Update branch** also releases them, because that commit is attributed to you; otherwise click **Approve and run**. It needs a code-owner approval like any other PR.
- The unit lane does not run on a Release PR, whichever of those two paths you take, and `🧪 Tests` still reports satisfied. A Release PR only bumps the version and rewrites the changelog. `Lint`, `reviewdog`, CodeQL and the PR-title check still run. Label the PR `run-tests` if you want the unit lane anyway.
- Merge the Release PR. That creates the tag and the GitHub Release, and `proxy.golang.org` picks the tag up. There is no separate publish step. The unit lane does run before the tag: `release.yml` calls it on the merge commit, and a failure there leaves `autorelease: pending` set and needs the manual recovery below. Integration tests are advisory and gate none of it.
- The unit lane does not run on a Release PR, whichever of those two paths you take, and `🧪 Tests` still reports satisfied. A Release PR only bumps the version and rewrites the changelog. The skip only applies while the diff is nothing but what release-please writes, down to the version line in each version file, so a code or dependency change pushed onto a Release PR by hand runs the unit lane like any other PR. `Lint`, `reviewdog`, CodeQL and the PR-title check still run. Label the PR `run-tests` if you want the unit lane anyway.
- Merge the Release PR. That creates the tag and the GitHub Release, and `proxy.golang.org` picks the tag up. There is no separate publish step and no further test run: the Release PR adds only the version bump and changelog to an already-tested `main`. A hotfix release from `N.x` runs the unit lane first, since its commits were pushed without a PR. Integration tests are advisory and gate none of it.

Only `feat`, `fix`, `perf` and breaking changes produce a release (`revert` may also). A window of only `chore`, `ci`, `docs`, `test`, `refactor`, `style` or `build` commits produces no Release PR, which is intended.

Expand Down
Loading