mirror bufbuild/buf-action@v1.1.1
Buf action CI / build (push) Canceled after 0s
Buf action CI / test-setup (push) Canceled after 0s
Buf action CI / test-setup-latest (push) Canceled after 0s
Buf action CI / test-setup-macos (push) Canceled after 0s
Buf action CI / test-setup-windows (push) Canceled after 0s
Buf action CI / test-setup-self-hosted (push) Canceled after 0s
Buf action CI / test-empty-build (push) Canceled after 0s
Buf action CI / test-lint (push) Canceled after 0s
Buf action CI / test-format (push) Canceled after 0s
Buf action CI / test-breaking (push) Canceled after 0s
Buf action CI / test-comment (push) Canceled after 0s
Buf action CI / test-push (push) Canceled after 0s
Buf action CI / test-push-token-only (push) Canceled after 0s
Buf action CI / test-push-unnamed (push) Canceled after 0s
Buf action CI / test-archive (push) Canceled after 0s
Buf action CI / test-archive-root (push) Canceled after 0s
Buf action CI / test-archive-unnamed (push) Canceled after 0s

This commit is contained in:
action-mirror committed 2026-11-09 01:03:09 +08:00
commit 38d0d7e18f
50 files changed
+85157

No files matched your search

+1
View File
@@ -0,0 +1 @@
dist/*.js binary
+10
View File
@@ -0,0 +1,10 @@
<svg width="85" height="80" viewBox="0 0 85 80" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="25.4114" y="20.7058" width="8.47059" height="59.2941" fill="#0E5DF5"/>
<rect x="42.3533" y="20.7058" width="8.47059" height="59.2941" fill="#0E5DF5"/>
<rect x="59.2942" y="20.7058" width="8.47059" height="59.2941" fill="#0E5DF5"/>
<rect x="76.2352" y="20.7058" width="8.47059" height="59.2941" fill="#0E5DF5"/>
<rect width="8.47059" height="59.2941" fill="#5FDCFF"/>
<rect x="16.9409" width="8.47059" height="59.2941" fill="#5FDCFF"/>
<rect x="33.8821" width="8.47059" height="59.2941" fill="#5FDCFF"/>
<rect x="50.8237" width="8.47059" height="59.2941" fill="#5FDCFF"/>
</svg>

After

Width:  |  Height:  |  Size: 683 B

+6
View File
@@ -0,0 +1,6 @@
version: 2
updates:
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "weekly"
+23
View File
@@ -0,0 +1,23 @@
name: Add issues and PRs to project
on:
issues:
types:
- opened
- reopened
- transferred
pull_request_target:
types:
- opened
- reopened
issue_comment:
types:
- created
permissions:
issues: read
pull-requests: read
repository-projects: write
jobs:
call-workflow-add-to-project:
name: Call workflow to add issue to project
uses: bufbuild/base-workflows/.github/workflows/add-to-project.yaml@main
secrets: inherit
+394
View File
@@ -0,0 +1,394 @@
name: Buf action CI
on:
push:
pull_request:
branches:
- main
permissions:
contents: read
pull-requests: write
env:
BUF_VERSION: "1.35.0" # Minimum version we support.
BUF_MODULE: ${{ vars.BUF_MODULE }}
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Build
run: |
make build
- name: Generate
run: |
make generate
- name: Check diff
run: |
make checkgenerate
test-setup:
runs-on: ubuntu-latest
needs: build
steps:
- uses: actions/checkout@v4
- uses: ./
with:
setup_only: true
- run: buf --version | grep $BUF_VERSION
test-setup-latest:
runs-on: ubuntu-latest
needs: test-setup
env:
BUF_VERSION: "" # Use the latest version
steps:
- uses: actions/checkout@v4
- uses: ./
with:
setup_only: true
- run: buf --version
test-setup-macos:
needs: test-setup
if: github.event_name == 'push'
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
- uses: ./
with:
setup_only: true
- run: buf --version | grep $BUF_VERSION
test-setup-windows:
needs: test-setup
if: github.event_name == 'push'
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- uses: ./
with:
setup_only: true
- run: buf --version | Select-String -Pattern $env:BUF_VERSION -CaseSensitive -SimpleMatch
test-setup-self-hosted:
needs: test-setup
# Disable this job, only for testing.
if: false
runs-on: self-hosted
steps:
- uses: actions/checkout@v4
- uses: ./
with:
setup_only: true
- run: buf --version | grep $BUF_VERSION
test-empty-build:
runs-on: ubuntu-latest
needs: build
steps:
- uses: actions/checkout@v4
- uses: ./
with:
lint: false
format: false
breaking: false
push: false
archive: false
pr_comment: false
continue-on-error: true # build fails
test-lint:
runs-on: ubuntu-latest
needs: build
steps:
- uses: actions/checkout@v4
- run: |
mkdir -p proto/test/v1
printf 'version: v2\nmodules:\n - path: proto' > buf.yaml
printf 'syntax = "proto3"; package test.v1; message A { string a = 1; }' > proto/test/v1/test.proto
- uses: ./
with:
lint: true
format: false
breaking: false
push: false
archive: false
pr_comment: false
test-format:
runs-on: ubuntu-latest
needs: build
steps:
- uses: actions/checkout@v4
- run: |
mkdir -p proto
printf 'version: v2\nmodules:\n - path: proto' > buf.yaml
printf 'syntax = "proto3";\n\nmessage A {\n string a = 1;\n}\n' > proto/test.proto
- uses: ./
with:
lint: false
format: true
breaking: false
push: false
archive: false
pr_comment: false
test-breaking:
runs-on: ubuntu-latest
needs: build
steps:
- uses: actions/checkout@v4
- run: |
mkdir -p proto
printf 'version: v2\nmodules:\n - path: proto' > buf.yaml
printf 'syntax = "proto3"; message A { string a = 1; }' > proto/test.proto
- uses: ./
with:
lint: false
format: false
breaking: true
breaking_against: .
push: false
archive: false
pr_comment: false
test-comment:
runs-on: ubuntu-latest
# Only run on pull requests from the same repository, comments from forks are not tested. Dependabot is skipped.
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository && github.actor != 'dependabot[bot]'
needs:
- test-lint
- test-format
- test-breaking
steps:
- uses: actions/checkout@v4
- run: |
# Lint skipped, format fails, breaking succeeds
mkdir -p proto
printf 'version: v2\nmodules:\n - path: proto' > buf.yaml
printf 'syntax = "proto3"; message A { string a = 1; }' > proto/test.proto
- uses: ./
continue-on-error: true # format fails
with:
token: ${{ secrets.BUF_TOKEN }}
lint: false # lint skipped
format: true
breaking: true
breaking_against: .
push: false
archive: false
pr_comment: true
- uses: actions/github-script@v7
with:
script: |
const expects = [
"The latest Buf updates on your PR.",
"✅ passed",
"❌ failed (1)",
"⏩ skipped",
];
const commentTag = "<!-- buf Buf action CI:test-comment -->";
const { owner, repo } = context.repo;
const prNumber = context.payload.pull_request?.number;
if (!prNumber) {
core.setFailed('No pull request number found.');
return;
}
const comments = await github.paginate(github.rest.issues.listComments, {
owner: owner,
repo: repo,
issue_number: prNumber,
});
const previousComment = comments.find((comment) =>
comment.body?.includes(commentTag),
);
if (!previousComment) {
core.setFailed('Missing comment on pull request.');
return;
}
for (const line of expects) {
if (!previousComment.body?.includes(line)) {
core.setFailed(`Comment does not include expected line: ${line}`);
return;
}
}
await github.rest.issues.updateComment({
owner: owner,
repo: repo,
comment_id: previousComment.id,
body: `${previousComment.body}\nNB: This is a test comment.`,
});
console.log(await github.graphql(`
mutation {
minimizeComment(input:{subjectId:"${previousComment.node_id}",classifier:RESOLVED}) {
clientMutationId
minimizedComment {
isMinimized
minimizedReason
viewerCanMinimize
}
}
}
`))
core.info('Updated comment.');
test-push:
runs-on: ubuntu-latest
if: ${{ github.event_name == 'push' }} # --git-metadata is only available on push
needs: build
steps:
- uses: actions/checkout@v4
- run: |
mkdir -p proto/foo/v1
printf "version: v2\nmodules:\n - path: proto\n name: ${BUF_MODULE}\n" > buf.yaml
printf "syntax = \"proto3\";\npackage foo.v1;\nmessage Bar {}\n" > proto/foo/v1/bar.proto
- uses: ./
with:
token: ${{ secrets.BUF_TOKEN }}
lint: false
format: false
breaking: false
push: true
archive: false
pr_comment: false
test-push-token-only:
needs: build
runs-on: ubuntu-latest
if: ${{ github.event_name == 'push' }} # --git-metadata is only available on push
steps:
- uses: actions/checkout@v4
- run: |
mkdir -p proto/foo/v1
printf "version: v2\nmodules:\n - path: proto\n name: ${BUF_MODULE}\n" > buf.yaml
printf "syntax = \"proto3\";\npackage foo.v1;\nmessage Bar {}\n" > proto/foo/v1/bar.proto
- uses: ./
with:
token: ${{ secrets.BUF_TOKEN }}
lint: false
format: false
breaking: false
push: true
archive: false
pr_comment: false
test-push-unnamed:
needs: build
runs-on: ubuntu-latest
if: ${{ github.event_name == 'push' }} # --git-metadata is only available on push
steps:
- uses: actions/checkout@v4
# Test v2 buf.yaml config.
- run: |
mkdir -p proto/foo/v1
printf "version: v2\nmodules:\n - path: proto\n" > buf.yaml
printf "syntax = \"proto3\";\npackage foo.v1;\nmessage Bar {}\n" > proto/foo/v1/bar.proto
- uses: ./
with:
token: ${{ secrets.BUF_TOKEN }}
lint: false
format: false
breaking: false
push: true
archive: false
pr_comment: false
# Test v1 buf.yaml config.
- run: |
rm -rf proto
mkdir -p proto/foo/v1
printf "version: v1\n" > proto/buf.yaml
printf "syntax = \"proto3\";\npackage foo.v1;\nmessage Bar {}\n" > proto/foo/v1/bar.proto
- uses: ./
with:
token: ${{ secrets.BUF_TOKEN }}
lint: false
format: false
breaking: false
push: true
archive: false
pr_comment: false
input: proto
# Test v2 with multiple modules, one without a name.
- run: |
mkdir -p foo/foo/v1 bar/bar/v1
printf "syntax = \"proto3\";\npackage foo.v1;\nmessage Bar {}\n" > foo/foo/v1/bar.proto
printf "syntax = \"proto3\";\npackage bar.v1;\nmessage Foo {}\n" > bar/bar/v1/foo.proto
printf "version: v2\nmodules:\n - path: foo\n name: ${BUF_MODULE}\n - path: bar\n" > buf.yaml
- uses: ./
with:
token: ${{ secrets.BUF_TOKEN }}
lint: false
format: false
breaking: false
push: true
archive: false
pr_comment: false
test-archive:
if: github.event_name == 'push' && github.ref_name != github.event.repository.default_branch
runs-on: ubuntu-latest
needs:
- test-push
- test-push-token-only
- test-push-unnamed
steps:
- uses: actions/checkout@v4
- run: |
mkdir -p proto/foo/v1
printf "version: v2\nmodules:\n - path: proto\n name: ${BUF_MODULE}\n" > buf.yaml
printf "syntax = \"proto3\";\npackage foo.v1;\nmessage Bar {}\n" > proto/foo/v1/bar.proto
- uses: ./
with:
token: ${{ secrets.BUF_TOKEN }}
lint: false
format: false
breaking: false
push: false
archive: true
pr_comment: false
test-archive-unnamed:
if: github.event_name == 'push'
runs-on: ubuntu-latest
needs:
- test-push
- test-push-token-only
- test-push-unnamed
steps:
- uses: actions/checkout@v4
- run: |
mkdir -p proto/foo/v1
printf "version: v2\nmodules:\n - path: proto\n" > buf.yaml
printf "syntax = \"proto3\";\npackage foo.v1;\nmessage Bar {}\n" > proto/foo/v1/bar.proto
- uses: ./
with:
token: ${{ secrets.BUF_TOKEN }}
lint: false
format: false
breaking: false
push: false
archive: true
pr_comment: false
- run: |
rm -rf proto
mkdir -p proto/foo/v1
printf "version: v1\n" > proto/buf.yaml
printf "syntax = \"proto3\";\npackage foo.v1;\nmessage Bar {}\n" > proto/foo/v1/bar.proto
- uses: ./
with:
token: ${{ secrets.BUF_TOKEN }}
lint: false
format: false
breaking: false
push: false
archive: true
pr_comment: false
input: proto
test-archive-root:
# Check the archive command parses non-module names.
runs-on: ubuntu-latest
needs:
- test-archive
steps:
- uses: actions/checkout@v4
- run: |
mkdir -p foo/v1
printf "version: v2\nname: ${BUF_MODULE}\n" > buf.yaml
printf "syntax = \"proto3\";\npackage foo.v1;\nmessage Bar {}\n" > foo/v1/bar.proto
- uses: ./
with:
token: ${{ secrets.BUF_TOKEN }}
lint: false
format: false
breaking: false
push: true # Push to re-create.
archive: true # Archive the pushed module.
comment: false
+24
View File
@@ -0,0 +1,24 @@
name: Dependabot Rebuild
on:
pull_request_target:
types: [opened, synchronize]
permissions:
contents: write
pull-requests: read
jobs:
build:
if: ${{ github.actor == 'dependabot[bot]' }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: rebuild
run: |
git remote set-url origin https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/$GITHUB_REPOSITORY
git config user.name github-actions
git config user.email github-actions@github.com
make build
git add -A
git commit -m "[dependabot skip] build" || true
git push -u origin HEAD:${{ github.event.pull_request.head.ref }}
@@ -0,0 +1,12 @@
name: Bypass review in case of emergency
on:
pull_request:
types:
- labeled
permissions:
pull-requests: write
jobs:
approve:
if: github.event.label.name == 'Emergency Bypass Review'
uses: bufbuild/base-workflows/.github/workflows/emergency-review-bypass.yaml@main
secrets: inherit
@@ -0,0 +1,13 @@
name: PR Approval Bypass Notifier
on:
pull_request:
types:
- closed
branches:
- main
permissions:
pull-requests: read
jobs:
approval:
uses: bufbuild/base-workflows/.github/workflows/notify-approval-bypass.yaml@main
secrets: inherit
+18
View File
@@ -0,0 +1,18 @@
name: Lint PR Title
# Prevent writing to the repository using the CI token.
# Ref: https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#permissions
permissions:
pull-requests: read
on:
pull_request:
# By default, a workflow only runs when a pull_request's activity type is opened,
# synchronize, or reopened. We explicity override here so that PR titles are
# re-linted when the PR text content is edited.
types:
- opened
- edited
- reopened
- synchronize
jobs:
lint:
uses: bufbuild/base-workflows/.github/workflows/pr-title.yaml@main
+5
View File
@@ -0,0 +1,5 @@
/.idea/
/.tmp/
/.vscode/
/node_modules/
.DS_Store
+2
View File
@@ -0,0 +1,2 @@
# Ignore artifacts
dist
+1
View File
@@ -0,0 +1 @@
{}
+201
View File
@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2024-2025 Buf Technologies, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+144
View File
@@ -0,0 +1,144 @@
# Migration Guide
This guide explains what changed when migrating from the individual Buf actions
([buf-setup-action][buf-setup], [buf-breaking-action][buf-breaking],
[buf-lint-action][buf-lint], [buf-push-action][buf-push]) to the new
consolidated `buf-action`.
## [buf-setup-action][buf-setup]
To migrate from `buf-setup-action` to `buf-action`, refer to the table below for the necessary changes:
| Old Parameter | New Parameter | Description |
|:--------------------|:--------------------|:--------------------|
| `version` | `version` | Unchanged. Version of `buf` CLI to install. |
| `buf_user` | N/A | Removed. The username is not required for login. |
| `buf_api_token` | `token` | Renamed. Buf API token for BSR requests. |
| `buf_domain` | `domain` | Renamed. Buf domain for BSR requests. Enterprise only. |
| N/A | `setup_only` | Added. Installs `buf` and optionally login to the BSR but no additional commands will be run. Set to `true` to make `buf-action` behave like `buf-setup-action`. Defaults to `false`. For more details, refer to the setup-only example in the [README](./README.md#setup-only) |
| N/A | `github_action` | Added. Github actor for API requests. Defaults to the current actor in the Github context. |
| `github_token` | `github_token` | Unchanged. GitHub token for API requests. Defaults to token in the Github Context. |
Example migration:
```diff
steps:
- uses: actions/checkout@v4
- - uses: bufbuild/buf-setup-action@v1.45.0
+ - uses: bufbuild/buf-action@v1
with:
- buf_user: ${{ secrets.buf_user }}
- buf_api_token: ${{ secrets.buf_api_token }}
+ version: '1.45.0' # Optional. Defaults to the latest release.
+ token: ${{ secrets.buf_api_token }}
+ setup_only: true
- run: buf --version
```
> [!NOTE]
> The `buf` CLI version is no longer dependent on the action version.
## [buf-breaking-action][buf-breaking]
To migrate from `buf-breaking-action` to `buf-action`, refer to the table below for the necessary changes:
| Old Parameter | New Parameter | Description |
|:--------------------|:--------------------|:-----------------------------------------------------------------|
| `input` | `input` | Unchanged. The input directory to build. Defaults to the current directory. See the [README](./README.md#specify-the-input-directory) for more details on specifying inputs. |
| `against` | `breaking_against` | Renamed. Specifies the reference to check breaking changes against. We recommend not setting this parameter and instead relying on the default behavior. |
| `buf_input_https_username`| N/A | Removed. The username is not required for login. |
| `buf_input_https_password`| N/A | Removed. To support multiple registries use the mulitple format of `BUF_TOKEN` as an enviornemt variable. See the [buf token formats documentation][buf-token-formats] for more details. |
| `buf_token` | `token` | Renamed. Buf API token for BSR requests. |
Example migration:
```diff
steps:
- uses: actions/checkout@v4
- - uses: bufbuild/buf-setup-action@v1
- - uses: bufbuild/buf-breaking-action@v1
+ - uses: bufbuild/buf-action@v1
with:
- buf_token: ${{ secrets.BUF_TOKEN }}
+ token: ${{ secrets.BUF_TOKEN }}
- against: 'https://github.com/acme/weather.git#branch=main'
+ # This example shows how to maintain behavior when migrating, but we recommend unsetting this parameter and relying on the default behavior instead.
+ breaking_against: 'https://github.com/acme/weather.git#branch=main'
```
## [buf-lint-action][buf-lint]
To migrate from `buf-lint-action` to `buf-action`, refer to the table below for the necessary changes.
| Old Parameter | New Parameter | Description |
|:--------------------|:--------------------|:-----------------------------------------------------------------|
| `input` | `input` | Unchanged. The input directory to lint. Defaults to the current directory. See the [README](./README.md#specify-the-input-directory) for more details on specifying inputs. |
| `buf_token` | `token` | Renamed. Buf API token for BSR requests. |
Example migration:
```diff
steps:
- uses: actions/checkout@v4
- - uses: bufbuild/buf-setup-action@v1
- - uses: bufbuild/buf-lint-action@v1
+ - uses: bufbuild/buf-action@v1
with:
input: proto
- buf_token: ${{ secrets.BUF_TOKEN }}
+ token: ${{ secrets.BUF_TOKEN }}
```
## [buf-push-action][buf-push]
To migrate from `buf-push-action` to `buf-action`, refer to the table below for the necessary changes:
| Old Parameter | New Parameter | Description |
|:--------------------|:--------------------|:-----------------------------------------------------------------|
| `input` | `input` | Unchanged. The input directory to push. Defaults to the current directory. See the [README](./README.md#specify-the-input-directory) for more details on specifying inputs. |
| `buf_token` | `token` | Renamed. Buf API token for BSR requests. |
| `draft` | N/A | Removed. Drafts are deprecated in favor of labels. Labels are set using [git metadata][git-metadata]. |
| `create_visibility` | N/A | Removed. Repositories will always be created with `private` visibility if they do not exist. |
| N/A | `push_disable_create`| Added. Disables automatic creation of repositories. Defaults to `false`. |
Example migration:
```diff
steps:
- uses: actions/checkout@v4
- - uses: bufbuild/buf-setup-action@v1
- - uses: bufbuild/buf-push-action@v1
+ - uses: bufbuild/buf-action@v1
with:
- buf_token: ${{ secrets.BUF_TOKEN }}
+ token: ${{ secrets.BUF_TOKEN }}
- create_visibility: private
- draft: ${{ github.ref_name != 'main'}}
```
To further customize the push action call the `buf push` command directly.
For example:
```diff
steps:
- uses: actions/checkout@v4
- uses: bufbuild/buf-action@v1
with:
token: ${{ secrets.BUF_TOKEN }}
+ setup_only: true # Disables default behavior.
+ - if: ${{ github.event_name == 'push' }}
+ run: buf push --error-format github-actions --create --git-metadata
```
> [!NOTE]
> Labels are a new feature in the `buf` CLI. For more information, see the [commits and labels][commits-labels] documentation.
[buf]: https://buf.build
[buf-setup]: https://github.com/marketplace/actions/buf-setup
[buf-breaking]: https://github.com/marketplace/actions/buf-breaking
[buf-cli]: https://github.com/bufbuild/buf
[buf-lint]: https://github.com/marketplace/actions/buf-lint
[buf-push]: https://github.com/marketplace/actions/buf-push
[git-metadata]: https://buf.build/docs/reference/cli/buf/push/?h=git+metadata#git-metadata
[buf-token-formats]: https://buf.build/docs/bsr/authentication/#buf_token-formats
[commits-labels]: https://buf.build/docs/concepts/commits-labels/
+61
View File
@@ -0,0 +1,61 @@
# See https://tech.davis-hansson.com/p/make/
SHELL := bash
.DELETE_ON_ERROR:
.SHELLFLAGS := -eu -o pipefail -c
.DEFAULT_GOAL := all
MAKEFLAGS += --warn-undefined-variables
MAKEFLAGS += --no-builtin-rules
MAKEFLAGS += --no-print-directory
BUF_VERSION ?= 1.50.1
UNAME_S := $(shell uname -s)
ifeq ($(UNAME_S),Darwin)
SED_I := sed -E -i ''
else
SED_I := sed -E -i
endif
.PHONY: help
help: ## Describe useful make targets
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "%-30s %s\n", $$1, $$2}'
PHONY: all
all: ## Format, Lint and build (default)
$(MAKE) build
.PHONY: format
format: node_modules
npm run format
.PHONY: lint
lint: node_modules
npm run lint
.PHONY: build
build: node_modules format lint
npm run build
.PHONY: updateversion
updateversion:
ifndef VERSION
$(error "VERSION must be set")
endif
ifndef VERSION_SHORT
$(error "VERSION_SHORT must be set")
endif
$(SED_I) "s/version: [0-9]+\.[0-9]+\.[0-9]+/version: $(BUF_VERSION)/g" action.yml README.md examples/*.yaml examples/*/*.yaml
$(SED_I) "s/buf-action@v[0-9]+(\.[0-9]+)?(\.[0-9]+)?/buf-action@v$(VERSION_SHORT)/g" README.md examples/*.yaml examples/*/*.yaml
$(SED_I) "s/\"version\": \"[0-9]+\.[0-9]+\.[0-9]+\"/\"version\": \"$(VERSION)\"/g" package.json
npm prune
.PHONY: generate
generate: node_modules ## Regenerate licenses
npm run generate
.PHONY: checkgenerate
checkgenerate:
@# Used in CI to verify that `make generate` doesn't produce a diff.
test -z "$$(git status --porcelain | tee /dev/stderr)"
node_modules: package-lock.json
npm ci
+320
View File
@@ -0,0 +1,320 @@
![The Buf logo](.github/buf-logo.svg)
# buf-action
[![ci](https://github.com/bufbuild/buf-action/actions/workflows/ci.yaml/badge.svg?branch=main)][ci]
[![slack](https://img.shields.io/badge/slack-buf-%23e01563)][slack]
This GitHub action makes it easy to run [`buf`][buf] within a workflow to check for
[build](https://buf.build/docs/reference/cli/buf/build),
[lint](https://buf.build/docs/lint/overview),
[format](https://buf.build/docs/format/style),
and [breaking change](https://buf.build/docs/breaking/overview) errors,
as well as to automatically [publish schema changes](https://buf.build/docs/bsr/module/publish) to the [Buf Schema Registry (BSR)](https://buf.build/product/bsr).
![Annotations example for lint and breaking changes](./static/img/annotations-example.png "Annotations example")
## Usage
To use this action with the recommended default behavior, create a new `.github/workflows/buf-ci.yaml` file in your repository with the following content:
```yaml
name: Buf CI
on:
push:
pull_request:
types: [opened, synchronize, reopened, labeled, unlabeled]
delete:
permissions:
contents: read
pull-requests: write
jobs:
buf:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: bufbuild/buf-action@v1
with:
token: ${{ secrets.BUF_TOKEN }}
```
### Default behavior
When you push a Git commit, tag, or branch to GitHub, the action will [push named modules to the BSR](https://buf.build/docs/bsr/module/publish) using `buf push`.
On a pull request, the action will run all checks (using `buf build`, `buf lint`, `buf format`, `buf breaking`) and then post a [summary comment](#summary-comment) on the PR.
When you delete a Git branch or tag, the action will archive the corresponding label on the BSR.
### Configuration
To customize the behavior of the action, you can set the following parameters in the workflow file.
Add these parameters under the `with` section of the `uses` step in the workflow file.
```yaml
- uses: bufbuild/buf-action@v1
with:
...
```
| Parameter | Description | Default |
|:--------------------------------|:---------------------------------------------------|:-------------------|
| `version` | Version of the `buf` CLI to use. | Latest [version][buf-releases] |
| `token` | API token for [logging into the BSR](https://buf.build/docs/bsr/authentication). | |
| `domain` | Domain for logging into the BSR, enterprise only.| `buf.build` |
| `input` | [Input](https://buf.build/docs/reference/inputs) for the `buf` command. | |
| `paths` | Limit to specific files or directories (separated by newlines). | |
| `exclude_imports` | Exclude files imported by the target modules. | False |
| `exclude_paths` | Exclude specific files or directories, e.g. "proto/a/a.proto", "proto/a" (separated by newlines). | |
| `pr_comment` | Comment the results on the pull request. The workflow and job name combination must be unique. | Only on pull requests (non forks) |
| `format` | Whether to run the formatting step. | Runs on pushes to Git PR |
| `lint` | Whether to run the linting step. | Runs on pushes to Git PR |
| `breaking` | Whether to run the breaking change detection step. | Runs on pushes to Git PR |
| `breaking_against` | [Input](https://buf.build/docs/reference/inputs) to compare against. | Base of the PR or the commit before the event |
| `push` | Whether to run the push step. | Runs on Git pushes (non forks) |
| `push_disable_create` | Disables repository creation if it does not exist. | False |
| `archive` | Whether to run the archive step. | Runs on Git deletes (non forks) |
| `setup_only` | Setup only the `buf` environment, optionally logging into the BSR, but without executing other commands. | |
| `github_actor` | GitHub actor for API requests. | Actor from GitHub context |
| `github_token` | GitHub token for API requests. Ensures requests aren't rate limited | Token from GitHub context |
| `public_github_token` | GitHub token for github.com. Must be set when running on a private GitHub Enterprise instance to authenticate requests, otherwise ignored. | |
### Skip the breaking change detection step
By default, the action runs the breaking change detection step on every pull request.
To skip this step, add the label `buf skip breaking` to the PR.
![Skip breaking changes example](./static/img/skip-breaking-example.png "Skip breaking changes example")
Ensure the workflow file includes the `pull_request` event types `labeled` and `unlabeled` so checks re-run on label changes.
To disable the ability to skip breaking change checks via a label, set the `breaking` parameter to the value `${{ github.event_name == 'pull_request' }}` so it runs on all PRs.
See [examples/disable-skip/buf-ci.yaml](examples/disable-skip/buf-ci.yaml) for an example.
### Disable steps
To disable parts of the workflow, each step corresponds to a boolean flag in the parameters.
For example to disable formatting set the parameter `format` to `false`:
```yaml
- uses: bufbuild/buf-action@v1
with:
format: false
```
See [action.yml](action.yml) for all available parameters.
### Versioning
For reproducible builds, you can pin to an explicit version of `buf` by setting `version`.
```yaml
- uses: bufbuild/buf-action@v1
with:
version: 1.50.1
```
If no version is specified in the workflow config, the action will resolve the version in order of precedence:
- A version specified in the environment variable `${BUF_VERSION}`.
- The version of `buf` that is already installed on the runner (if it exists).
- The latest version of the `buf` binary from the official releases on GitHub.
### Authentication
[Publishing schemas](https://buf.build/docs/bsr/module/publish) to the BSR provides a seamless
way for consumers of your APIs to generate code.
Authenticating with the BSR is required for both the push and archive label steps.
Generate a token from the [BSR UI](https://buf.build/docs/bsr/authentication#create-an-api-token) and add it to the [repository secrets](https://docs.github.com/en/actions/reference/encrypted-secrets).
To authenticate with the BSR, set the API token as the parameter `token`:
```yaml
- uses: bufbuild/buf-action@v1
with:
token: ${{ secrets.BUF_TOKEN }}
```
A post-action step will logout from the BSR to ensure the token is not stored in the `.netrc` file.
For more information on authentication, see the [BSR Authentication Reference](https://buf.build/docs/bsr/authentication).
### Summary comment
The action reports the status of the most recent checks in a comment on each pull request.
![Comment example showing the GitHub summary](./static/img/comment-example.png "Summary comment example")
To disable the comment, set the parameter `comment` to `false` and remove the permission `pull_request: write` as this is no longer required.
```diff
name: Buf CI
on:
push:
pull_request:
types: [opened, synchronize, reopened, labeled, unlabeled]
delete:
permissions:
contents: read
- pull-requests: write
jobs:
buf:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: bufbuild/buf-action@v1
with:
token: ${{ secrets.BUF_TOKEN }}
+ pr_comment: false
```
### Specify the input directory
To run the action for inputs not declared at the root of the repository,
set the parameter `input` to the directory of your `buf.yaml` file.
```yaml
- uses: bufbuild/buf-action@v1
with:
input: <path/to/module>
```
Breaking change detection by default will use the `input` value as a subdirectory for the breaking against value.
To customize this behavior, set the parameter `breaking_against` to the desired input.
```yaml
- uses: bufbuild/buf-action@v1
with:
input: <path/to/module>
breaking_against: ${{ github.event.repository.clone_url }}#format=git,commit=${{ github.event.pull_request.base.sha }},subdir=<path/to/module>
```
Alternatively, you can checkout the base for the breaking comparison to a local folder
and then set the value of `breaking_against` to the path of the base.
```yaml
- uses: actions/checkout@v4
with:
path: head
- uses: actions/checkout@v4
with:
path: base
ref: ${{ github.event.pull_request.base.sha }}
- uses: bufbuild/buf-action@v1
with:
input: head/<path/to/module>
breaking_against: base/<path/to/module>
```
For more information on inputs, see the [Buf Inputs Reference](https://buf.build/docs/reference/inputs).
### Setup only
To only setup the action without running any commands, set the parameter `setup_only` to `true`.
This will install `buf` and optionally login to the schema registry but no additional commands will be run.
Subsequent steps will have `buf` available in their $PATH and can invoke `buf` directly.
```yaml
- uses: bufbuild/buf-action@v1
with:
setup_only: true
- run: buf build --error-format github-actions
```
See the [only-setup.yaml](examples/only-setup/buf-ci.yaml) example.
### Customize when steps run
To trigger steps on different events use the GitHub action context to deduce the event type.
For example to enable formatting checks on both pull requests and push create an expression for the parameter `format`:
```yaml
- uses: bufbuild/buf-action@v1
with:
format: ${{ contains(fromJSON('["push", "pull_request"]'), github.event_name) }}
```
See [GitHub Actions expressions](https://docs.github.com/en/actions/learn-github-actions/expressions) documentation.
### Skip checks if commit message matches a specific pattern
To conditionally run checks based on user input, use the GitHub action context to check for the contents of the commit.
For example to disable breaking change detection on commits, create an expression on the parameter `breaking` to check the contents of the commit message:
```yaml
- uses: bufbuild/buf-action@v1
with:
breaking: |
contains(fromJSON('["push", "pull_request"]'), github.event_name) &&
!contains(github.event.head_commit.message, 'buf skip breaking')
```
See [GitHub Actions job context](https://docs.github.com/en/actions/reference/context-and-expression-syntax-for-github-actions#job-context) documentation.
### Only push on changes to APIs
To push only on changes to when your module changes, restrict the push step for any changes to `buf` related files.
This can be achieved by using the `paths` filter on the `push` event.
```yaml
push:
paths:
- '**.proto'
- '**/buf.yaml'
- '**/buf.lock'
- '**/buf.md'
- '**/README.md'
- '**/LICENSE'
```
See the [push-on-changes.yaml](examples/push-on-changes/buf-ci.yaml) example.
### Example workflows
Check out the [examples](examples) directory for more detailed workflows.
## Migrating from individual Buf actions
If you're currently using any of our individual actions
([buf-setup-action][buf-setup], [buf-breaking-action][buf-breaking], [buf-lint-action][buf-lint], [buf-push-action][buf-push]),
we recommend migrating to this consolidated action that has additional capabilities. Benefits to migrating include:
- Less configuration and setup, with built-in best practices.
- Enhanced integration with Git data when pushing to the BSR.
- Status comments on pull requests.
- Easy configuration for custom behavior.
See the [migration guide](MIGRATION.md) for more information.
## Debugging
To debug the action, rerun the workflow with debug logging enabled.
This will run all buf commands with the `--debug` flag.
See the [re-run jobs with debug logging](https://github.blog/changelog/2022-05-24-github-actions-re-run-jobs-with-debug-logging/) for more information.
## Feedback and support
If you have any feedback or need support, please reach out to us on the [Buf Slack][slack],
or [GitHub Issues](https://github.com/bufbuild/buf-action/issues).
## Status: stable
This action is stable and ready for production use.
## Legal
Offered under the [Apache 2 license][license].
[buf]: https://buf.build
[buf-setup]: https://github.com/marketplace/actions/buf-setup
[buf-breaking]: https://github.com/marketplace/actions/buf-breaking
[buf-cli]: https://github.com/bufbuild/buf
[buf-lint]: https://github.com/marketplace/actions/buf-lint
[buf-push]: https://github.com/marketplace/actions/buf-push
[buf-releases]: https://github.com/bufbuild/buf/releases
[ci]: https://github.com/bufbuild/buf-action/actions/workflows/ci.yaml
[license]: https://github.com/bufbuild/bufisk/blob/main/LICENSE
[slack]: https://buf.build/links/slack
[bsr]: https://buf.build/docs/introduction
[push-event]: https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#push
[pull-request-event]: https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request
[delete-event]: https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#delete
+37
View File
@@ -0,0 +1,37 @@
# Releasing buf-action
This document outlines how to create a release of buf-action.
We follow the best practices outlined in the [GitHub Actions documentation][github-release-docs] and [GitHub Actions Toolkit tutorial][github-release-tutorial].
1. Clone the repo, ensuring you have the latest main.
2. On a new branch, run the following commands to update the version of the action:
```bash
VERSION=X.Y.Z VERSION_SHORT=X make updateversion
```
3. Open a PR titled "Prepare for vX.Y.Z". Once it's reviewed and CI passes, merge it.
*Make sure no new commits are merged until the release is complete.*
4. Create a new release using the Github UI:
- Under “Choose a tag”, type in “vX.Y.Z” to create a new tag for the release upon publish.
- Target the main branch.
- Title the Release “vX.Y.Z”.
- Click “set as latest release”.
- Click "Publish this Action to the GitHub Marketplace".
5. Publish the release.
6. Pull down the latest main and move the tags for major (vX) and minor (vX.Y) releases.
```bash
git tag -fa vX -m "Update vX tag"
git tag -fa vX.Y -m "Update vX.Y tag"
git push origin vX vX.Y --force
```
[github-release-docs]: https://docs.github.com/en/actions/creating-actions/releasing-and-maintaining-actions
[github-release-tutorial]: https://github.com/actions/toolkit/blob/master/docs/action-versioning.md
+148
View File
@@ -0,0 +1,148 @@
# Copyright 2024 Buf Technologies, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
name: "Buf Action"
author: "Buf"
description: |-
Run build, lint, format, and breaking change checks on Protobuf files, and publish schemas to the Buf Schema Registry.
branding:
icon: play
color: blue
inputs:
version:
description: |-
Version of the Buf CLI to use.
Example:
with:
version: 1.50.1
required: false
token:
description: |-
API token for logging into the BSR.
required: false
domain:
description: |-
Domain for logging into the BSR, enterprise only.
required: false
default: "buf.build"
github_actor:
description: |-
GitHub actor for API requests.
required: false
default: ${{ github.actor }}
github_token:
description: |-
GitHub token for the GitHub instance this action is running on.
required: false
default: ${{ github.token }}
public_github_token:
description: |-
GitHub token for github.com. Must be set when running on a private GitHub Enterprise instance to authenticate requests, otherwise ignored.
setup_only:
description: |-
Setup only the buf environment, optionally logging into the BSR, but without executing other commands.
required: false
default: "false"
pr_comment:
description: |-
Comment on the pull request with the results of each step. The workflow and job name combination must be unique.
Only runs on pull requests, for non forked repositories.
required: false
default: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository }}
input:
description: |-
Input for the buf command.
required: false
paths:
description: |-
Limit to specific files or directories (separated by newlines).
Example:
with:
paths: |
proto/a/a.proto
proto/a
required: false
exclude_paths:
description: |-
Exclude specific files or directories (separated by newlines).
Example:
with:
exclude_paths: |
proto/a/a.proto
proto/a
required: false
exclude_imports:
description: |-
Exclude files imported by the target modules.
required: false
default: "false"
lint:
description: |-
Whether to run the linting step.
Runs by default on pull requests.
required: false
default: ${{ github.event_name == 'pull_request' }}
format:
description: |-
Whether to run the formatting step.
Runs by default on pull requests.
required: false
default: ${{ github.event_name == 'pull_request' }}
breaking:
description: |-
Whether to run the breaking change detection step.
Runs by default on pull requests if the "buf skip breaking" label is not applied.
required: false
default: ${{ github.event_name == 'pull_request' && !contains(github.event.pull_request.labels.*.name, 'buf skip breaking') }}
breaking_against:
description: |-
Input to compare against for breaking change detection.
Defaults to the base branch of the pull request or the commit before the push.
required: false
push:
description: |-
Whether to run the push step. Runs by default on pushes, for non forked repositories.
required: false
default: ${{ github.event_name == 'push' }}
push_disable_create:
description: |-
Disables repository creation if it does not exist. Defaults to false.
required: false
default: "false"
archive:
description: |-
Whether to run the archive step. Runs by default on deletes, for non forked repositories.
required: false
default: ${{ github.event_name == 'delete' }}
outputs:
buf_version:
description: |-
Version of the Buf CLI used.
buf_path:
description: |-
Path to the Buf CLI binary.
runs:
using: "node20"
main: "./dist/index.js"
post: "./dist/post/index.js"
+48506
View File
File diff suppressed because it is too large. Load diff
+31972
View File
File diff suppressed because it is too large. Load diff
+24
View File
@@ -0,0 +1,24 @@
// Copyright 2024-2025 Buf Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import globals from "globals";
import pluginJs from "@eslint/js";
import tseslint from "typescript-eslint";
export default [
{ languageOptions: { globals: globals.node } },
pluginJs.configs.recommended,
...tseslint.configs.recommended,
];
+23
View File
@@ -0,0 +1,23 @@
# Examples
This directory contains examples of how to use the `buf` GitHub Action.
## Workflows
- [buf-ci.yaml](./buf-ci.yaml): Recommended configuration for using `buf` in a CI environment.
- [only-checks/buf-ci.yaml](./only-checks/buf-ci.yaml): How to use `buf` to only run checks on pull requests.
- [only-setup/buf-ci.yaml](./only-setup/buf-ci.yaml): How to use `buf` to only install `buf`.
- [only-setup-defaults/buf-ci.yaml](./only-setup-defaults/buf-ci.yaml): Showcases the default steps of the `buf` action.
- [only-sync/buf-ci.yaml](./only-sync/buf-ci.yaml): How to use `buf` to only sync the repository labels.
- [push-on-changes/buf-ci.yaml](./push-on-changes/buf-ci.yaml): Pushes changes only on detecting protobuf file changes.
- [skip-on-commits/buf-ci.yaml](./skip-on-commits/buf-ci.yaml): How to use commit messages to skip `buf` checks on push.
- [skip-on-labels/buf-ci.yaml](./skip-on-labels/buf-ci.yaml): How to use custom labels to skip `buf` checks on pull requests.
- [validate-push/buf-ci.yaml](./validate-push/buf-ci.yaml): Validate checks before pushing to the repository.
- [version-env/buf-ci.yaml](./version-env/buf-ci.yaml): Resolve the `buf` version from an environment variable (`BUF_VERSION`).
- [version-input/buf-ci.yaml](./version-input/buf-ci.yaml): Resolve the `buf` version from an action input (`version`).
- [version-latest/buf-ci.yaml](./version-latest/buf-ci.yaml): Use the latest `buf` version.
+18
View File
@@ -0,0 +1,18 @@
# This is a GitHub Actions workflow file that uses the buf-action to run Buf CI.
name: Buf CI
on:
push:
pull_request:
types: [opened, synchronize, reopened, labeled, unlabeled]
delete:
permissions:
contents: read
pull-requests: write
jobs:
buf:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: bufbuild/buf-action@v1
with:
token: ${{ secrets.BUF_TOKEN }}
+22
View File
@@ -0,0 +1,22 @@
# This workflow runs checks before pushing to the BSR.
name: Buf CI
on:
push:
delete:
permissions:
contents: read
jobs:
buf:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: bufbuild/buf-action@v1
with:
token: ${{ secrets.BUF_TOKEN }}
lint: ${{ github.event_name == 'push' }}
format: ${{ github.event_name == 'push' }}
breaking: ${{ github.event_name == 'push' && !contains(github.event.head_commit.message, 'buf skip breaking') }}
# Specify the branch to compare against for breaking change detection.
# The default for push is to compare to the commit before. We want to
# compare against the main branch for a GitHub flow style workflow.
breaking_against: .git#branch=main
+21
View File
@@ -0,0 +1,21 @@
# This workflow disables the use of labels for skipping checks. It does this by
# removing the expressions that check for the presence of the 'buf skip' labels.
# As a result, the pull_request types for labeled and unlabeled events can now
# also be removed (NB: the 'pull_request' default event types include opened,
# synchronize, and reopened).
name: Buf CI
on:
push:
pull_request:
delete:
permissions:
contents: read
jobs:
buf:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: bufbuild/buf-action@v1
with:
token: ${{ secrets.BUF_TOKEN }}
breaking: ${{ github.event_name == 'pull_request' }}
+22
View File
@@ -0,0 +1,22 @@
# This workflow shows the default configuration for GitHub Enterprise Server.
name: Buf CI
on:
push:
pull_request:
types: [opened, synchronize, reopened, labeled, unlabeled]
delete:
permissions:
contents: read
pull-requests: write
jobs:
buf:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: bufbuild/buf-action@v1
with:
token: ${{ secrets.BUF_TOKEN }}
# Specify the public GitHub token to authenticate requests to github.com.
# This is required when running on a private GitHub Enterprise instance.
# Will be used when resolving buf versions and downloading buf binaries.
public_github_token: ${{ secrets.PUBLIC_GITHUB_TOKEN }}
+14
View File
@@ -0,0 +1,14 @@
# This workflow runs checks on pull_requests only, without push and archive steps.
name: Buf CI
on:
pull_request:
types: [opened, synchronize, reopened, labeled, unlabeled]
permissions:
contents: read
pull-requests: write
jobs:
buf:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: bufbuild/buf-action@v1
+35
View File
@@ -0,0 +1,35 @@
# This file showcases the Buf CI workflow in a step by step breakdown.
# Based on the buf-ci.yaml file, this action walks through the Buf CI workflow
# and shows what each step does.
name: Buf CI
on:
push:
pull_request:
types: [opened, synchronize, reopened, labeled, unlabeled]
delete:
permissions:
contents: read
jobs:
buf:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: bufbuild/buf-action@v1
with:
setup_only: true
- env:
BUF_TOKEN: ${{ secrets.BUF_TOKEN }}
run: echo ${BUF_TOKEN} | buf registry login buf.build --token-stdin
- run: buf build --error-format=github-actions
- if: ${{ github.event_name == 'pull_request' && !contains(github.event.pull_request.labels.*.name, 'buf skip lint') }}
run: buf lint --error-format github-actions
- if: ${{ github.event_name == 'pull_request' && !contains(github.event.pull_request.labels.*.name, 'buf skip format') }}
run: buf format --diff --error-format github-actions --exit-code
- if: ${{ github.event_name == 'pull_request' && !contains(github.event.pull_request.labels.*.name, 'buf skip breaking') }}
run: |
buf breaking --error-format github-actions --against \
${{ github.event.repository.clone_url }}#format=git,tag=${{ github.event.pull_request.base.sha || github.event.before }}
- if: ${{ github.event_name == 'push' }}
run: buf push --error-format github-actions --create --git-metadata
- if: ${{ github.event_name == 'delete' }}
run: buf beta registry archive --label ${{ github.ref_name }}
+18
View File
@@ -0,0 +1,18 @@
# This file is used to setup the Buf CLI in the GitHub Actions environment.
# After the buf-action is run, the Buf CLI is available for use in subsequent steps.
name: Buf CI
on:
push:
permissions:
contents: read
jobs:
buf:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: bufbuild/buf-action@v1
with:
# Add the parameter token as a secret in your repository settings
# to authenticate with the Buf Schema Registry.
setup_only: true
- run: buf build --error-format=github-actions
+15
View File
@@ -0,0 +1,15 @@
# This workflow syncs the changes to the BSR without running any checks.
name: Buf CI
on:
push:
delete:
permissions:
contents: read
jobs:
buf:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: bufbuild/buf-action@v1
with:
token: ${{ secrets.BUF_TOKEN }}
+24
View File
@@ -0,0 +1,24 @@
# This workflow shows syncing changes only when the buf.yaml, README.md, LICENSE
# or .proto files are modified.
# Caution: Restricting push can cause unintended behavior. Review the paths carefully.
name: Buf CI
on:
push:
paths:
# Caution: This workflow could miss changes if the paths are not correctly specified.
- '**.proto'
- '**/buf.yaml'
- '**/buf.lock'
- '**/buf.md'
- '**/README.md'
- '**/LICENSE'
permissions:
contents: read
jobs:
buf:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: bufbuild/buf-action@v1
with:
token: ${{ secrets.BUF_TOKEN }}
+18
View File
@@ -0,0 +1,18 @@
# This workflow skips checks when `buf skip` is used in the commit message.
name: Buf CI
on:
push:
delete:
permissions:
contents: read
jobs:
buf:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: bufbuild/buf-action@v1
with:
token: ${{ secrets.BUF_TOKEN }}
lint: ${{ github.event_name == 'push' && !contains(github.event.head_commit.message, 'buf skip lint') }}
format: ${{ github.event_name == 'push' && !contains(github.event.head_commit.message, 'buf skip format') }}
breaking: ${{ github.event_name == 'push' && !contains(github.event.head_commit.message, 'buf skip breaking') }}
+20
View File
@@ -0,0 +1,20 @@
# This workflow skips checks when `buf skip` is applied to a PR.
name: Buf CI
on:
push:
pull_request:
types: [opened, synchronize, reopened, labeled, unlabeled]
delete:
permissions:
contents: read
jobs:
buf:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: bufbuild/buf-action@v1
with:
token: ${{ secrets.BUF_TOKEN }}
lint: ${{ github.event_name == 'pull_request' && !contains(github.event.pull_request.labels.*.name, 'buf skip lint') }}
format: ${{ github.event_name == 'pull_request' && !contains(github.event.pull_request.labels.*.name, 'buf skip format') }}
breaking: ${{ github.event_name == 'pull_request' && !contains(github.event.pull_request.labels.*.name, 'buf skip breaking') }}
+19
View File
@@ -0,0 +1,19 @@
# This workflow runs checks on push events before pushing the changes to the BSR.
name: Buf CI
on:
push:
delete:
permissions:
contents: read
jobs:
buf:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: bufbuild/buf-action@v1
with:
# The token paramater is required to authenticate with the Buf Schema Registry.
token: ${{ secrets.BUF_TOKEN }}
lint: ${{ github.event_name == 'push' }}
format: ${{ github.event_name == 'push' }}
breaking: ${{ github.event_name == 'push' }}
+17
View File
@@ -0,0 +1,17 @@
# This workflow downloads the version of buf specified in the env var $BUF_VERSION.
name: Buf CI
on:
push:
permissions:
contents: read
env:
BUF_VERSION: "1.32.0"
jobs:
buf:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: bufbuild/buf-action@v1
with:
setup_only: true
- run: buf version
+16
View File
@@ -0,0 +1,16 @@
# This workflow downloads the version of buf specified in the action input.
name: Buf CI
on:
push:
permissions:
contents: read
jobs:
buf:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: bufbuild/buf-action@v1
with:
setup_only: true
version: 1.50.1
- run: buf version
+15
View File
@@ -0,0 +1,15 @@
# This workflow downloads the latest version of buf as none is specified.
name: Buf CI
on:
push:
permissions:
contents: read
jobs:
buf:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: bufbuild/buf-action@v1
with:
setup_only: true
- run: buf version
+1881
View File
File diff suppressed because it is too large. Load diff
+52
View File
@@ -0,0 +1,52 @@
{
"name": "buf-action",
"version": "1.1.1",
"description": "GitHub Action for buf",
"main": "src/main.ts",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"format": "prettier --write 'src/*.{json,js,jsx,ts,tsx,css}' --log-level error",
"lint": "eslint --max-warnings 0 src",
"lint:fix": "eslint --max-warnings 0 --fix src",
"build": "ncc build -o dist src/main.ts && ncc build -o dist/post src/post.ts",
"generate": "license-header --ignore 'dist/**'"
},
"keywords": [
"actions",
"buf",
"proto",
"protobuf"
],
"author": "Buf Technologies, Inc.",
"license": "Apache-2.0",
"licenseHeader": {
"licenseType": "apache",
"yearRange": "2024-2025",
"copyrightHolder": "Buf Technologies, Inc."
},
"devDependencies": {
"@bufbuild/license-header": "^0.0.4",
"@eslint/js": "^9.23.0",
"@types/node": "^22.13.11",
"@types/semver": "^7.5.8",
"@vercel/ncc": "^0.38.3",
"eslint": "^9.23.0",
"globals": "^16.0.0",
"prettier": "3.5.3",
"typescript": "^5.7.3",
"typescript-eslint": "^8.27.0"
},
"dependencies": {
"@actions/core": "^1.11.1",
"@actions/exec": "^1.1.1",
"@actions/github": "^6.0.0",
"@actions/tool-cache": "^2.0.2",
"@buf/bufbuild_registry.bufbuild_es": "^2.2.3-20250116203702-1c024d64352b.1",
"@bufbuild/protobuf": "^2.2.5",
"@connectrpc/connect": "^2.0.1",
"@connectrpc/connect-web": "^2.0.1",
"@octokit/webhooks-types": "^7.6.1",
"parse-diff": "^0.11.1",
"semver": "^7.7.1"
}
}
+95
View File
@@ -0,0 +1,95 @@
// Copyright 2024-2025 Buf Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import * as core from "@actions/core";
import { context } from "@actions/github";
import { Context } from "@actions/github/lib/context";
import { GitHub } from "@actions/github/lib/utils";
// oldCommentTag is the previous tag used to identify the comment. This is
// temporary and will be removed in a future release.
const oldCommentTag = "<!-- Buf results -->";
// commentTag returns the tag used to identify the comment. This is a non-visible
// string injected into the comment body. It is unique to the workflow and job.
function commentTag(): string {
return `<!-- buf ${context.workflow}:${context.job} -->`;
}
// findCommentOnPR finds the comment on the PR that contains the Buf results.
// If the comment is found, it returns the comment ID. If the comment is not
// found, it returns undefined.
export async function findCommentOnPR(
context: Context,
github: InstanceType<typeof GitHub>,
): Promise<number | undefined> {
const { owner, repo } = context.repo;
const prNumber = context.payload.pull_request?.number;
if (!prNumber) {
core.info("This is not a PR, skipping finding comment");
return undefined;
}
const comments = await github.paginate(github.rest.issues.listComments, {
owner: owner,
repo: repo,
issue_number: prNumber,
});
const tag = commentTag();
const previousComment = comments.find(
(comment) =>
// TODO: Remove the old comment tag check in a future release.
comment.body?.includes(tag) || comment.body?.includes(oldCommentTag),
);
if (previousComment) {
core.info(`Found previous comment ${previousComment.id}`);
return previousComment.id;
}
return undefined;
}
// commentOnPR comments on the PR with the summary of the Buf results. The
// summary should be a markdown formatted string. This function returns true if
// the comment was successfully created or updated.
export async function commentOnPR(
context: Context,
github: InstanceType<typeof GitHub>,
commentID: number | undefined,
body: string,
): Promise<number | undefined> {
const { owner, repo } = context.repo;
const prNumber = context.payload.pull_request?.number;
if (!prNumber) {
core.info("This is not a PR, skipping commenting");
return undefined;
}
const content = {
owner: owner,
repo: repo,
body: body + commentTag(),
};
if (commentID) {
await github.rest.issues.updateComment({
...content,
comment_id: commentID,
});
core.info(`Updated comment ${commentID} on PR #${prNumber}`);
return commentID;
}
const comment = await github.rest.issues.createComment({
...content,
issue_number: prNumber,
});
core.info(`Commented ${comment.data.id} on PR #${prNumber}`);
return comment.data.id;
}
+56
View File
@@ -0,0 +1,56 @@
// Copyright 2024-2025 Buf Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import * as exec from "@actions/exec";
export interface ModuleName {
name: string; // Full module name
registry: string;
owner: string;
module: string;
}
// parseModuleNames extracts the module names from the given input. The input
// is a directory containing a buf.yaml file.
export async function parseModuleNames(
bufPath: string,
input: string,
): Promise<ModuleName[]> {
return exec
.getExecOutput(bufPath, ["config", "ls-modules", "--format", "name"], {
cwd: input,
})
.then((output) => {
const stdout = output.stdout.trim();
if (stdout === "") {
return [];
}
return stdout.split("\n").map((n) => parseModuleName(n));
});
}
// parseModuleName parses the module name into its registry, owner, and
// repository parts.
export function parseModuleName(moduleName: string): ModuleName {
const parts = moduleName.split("/");
if (parts.length != 3) {
throw new Error(`Invalid module name: ${moduleName}`);
}
return {
name: moduleName,
registry: parts[0],
owner: parts[1],
module: parts[2],
};
}
+108
View File
@@ -0,0 +1,108 @@
// Copyright 2024-2025 Buf Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import * as core from "@actions/core";
import * as github from "@actions/github";
import {
PushEvent,
PullRequestEvent,
DeleteEvent,
} from "@octokit/webhooks-types";
// Inputs are the inputs to the action, matching the inputs in the action.yml.
export interface Inputs {
version: string;
token: string;
domain: string;
setup_only: boolean;
pr_comment: boolean;
github_actor: string;
github_token: string;
public_github_token: string;
input: string;
paths: string[];
exclude_paths: string[];
exclude_imports: boolean;
lint: boolean;
format: boolean;
breaking: boolean;
breaking_against: string;
push: boolean;
push_disable_create: boolean;
archive: boolean;
archive_labels: string[];
}
// getInputs decodes the inputs from the environment variables.
export function getInputs(): Inputs {
const inputs: Inputs = {
version: core.getInput("version"),
token: core.getInput("token") || getEnv("BUF_TOKEN"),
domain: core.getInput("domain"),
setup_only: core.getBooleanInput("setup_only"),
pr_comment: core.getBooleanInput("pr_comment"),
github_actor: core.getInput("github_actor"),
github_token: core.getInput("github_token"),
public_github_token: core.getInput("public_github_token"),
// Inputs shared between buf steps.
input: core.getInput("input"),
paths: core.getMultilineInput("paths"),
exclude_paths: core.getMultilineInput("exclude_paths"),
exclude_imports: core.getBooleanInput("exclude_imports"),
// Inputs specific to buf steps.
lint: core.getBooleanInput("lint"),
format: core.getBooleanInput("format"),
breaking: core.getBooleanInput("breaking"),
breaking_against: core.getInput("breaking_against"),
push: core.getBooleanInput("push"),
push_disable_create: core.getBooleanInput("push_disable_create"),
archive: core.getBooleanInput("archive"),
archive_labels: [],
};
if (github.context.eventName === "push") {
const event = github.context.payload as PushEvent;
if (inputs.breaking_against === "") {
inputs.breaking_against = `${event.repository.clone_url}#format=git,commit=${event.before}`;
if (inputs.input) {
inputs.breaking_against += `,subdir=${inputs.input}`;
}
}
inputs.archive_labels.push(github.context.ref);
}
if (github.context.eventName === "pull_request") {
const event = github.context.payload as PullRequestEvent;
if (inputs.breaking_against === "") {
inputs.breaking_against = `${event.repository.clone_url}#format=git,commit=${event.pull_request.base.sha}`;
if (inputs.input) {
inputs.breaking_against += `,subdir=${inputs.input}`;
}
}
inputs.archive_labels.push(github.context.ref);
}
if (github.context.eventName === "delete") {
const event = github.context.payload as DeleteEvent;
inputs.archive_labels.push(event.ref);
}
return inputs;
}
// getEnv returns the case insensitive value of the environment variable.
// Prefers the lowercase version of the variable if it exists.
export function getEnv(name: string): string {
return (
process.env[name.toLowerCase()] ?? process.env[name.toUpperCase()] ?? ""
);
}
+172
View File
@@ -0,0 +1,172 @@
// Copyright 2024-2025 Buf Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import * as core from "@actions/core";
import * as exec from "@actions/exec";
import * as tc from "@actions/tool-cache";
import { GitHub } from "@actions/github/lib/utils";
import * as semver from "semver";
import { getEnv } from "./inputs";
// requiredVersion is the minimum version of buf required.
const requiredVersion = ">=1.35.0";
// installBuf installs the buf binary and returns the path to the binary. The
// versionInput should be an explicit version of buf.
export async function installBuf(
github: InstanceType<typeof GitHub> | undefined,
githubToken: string,
inputVersion: string,
): Promise<[string, string]> {
let resolvedVersion = resolveVersion(inputVersion);
if (resolvedVersion != "" && !tc.isExplicitVersion(resolvedVersion)) {
throw new Error(
`The version provided must be an explicit version: ${resolvedVersion}`,
);
}
// Check if the binary is already available.
const bufName = "buf";
const binName = process.platform === "win32" ? "buf.exe" : "buf";
const whichBuf = await exec.exec("which", [binName], {
ignoreReturnCode: true,
silent: true,
});
if (whichBuf === 0) {
const bufVersion = await exec.getExecOutput(binName, ["--version"], {
silent: true,
});
const version = bufVersion.stdout.trim();
if (
semver.satisfies(version, requiredVersion) &&
(resolvedVersion == "" || semver.eq(version, resolvedVersion))
) {
core.info(`Using buf (${version}) found in $PATH`);
return [binName, version];
}
}
if (resolvedVersion === "") {
if (!github) {
throw new Error(
`Unable to resolve the latest version of buf. Must set the "version" parameter or provide a "public_github_token" to authenticate requests.`,
);
}
resolvedVersion = await latestVersion(github);
}
if (!semver.satisfies(resolvedVersion, requiredVersion)) {
throw new Error(
`The resolved version of buf (${resolvedVersion}) does not satisfy the required version (${requiredVersion})`,
);
}
// Fetch the version of buf to use.
let cachePath = tc.find(bufName, resolvedVersion);
if (!cachePath) {
core.info(`Downloading buf (${resolvedVersion})`);
const downloadPath = await downloadBuf(resolvedVersion, githubToken);
await exec.exec("chmod", ["+x", downloadPath]);
cachePath = await tc.cacheFile(
downloadPath,
binName,
bufName,
resolvedVersion,
);
}
core.addPath(cachePath);
core.info(`Setup buf (${resolvedVersion}) at ${cachePath}`);
return [binName, resolvedVersion];
}
// resolveVersion from the input or environment.
function resolveVersion(versionSpec: string): string {
if (versionSpec) {
core.info(`Using the version of buf from the input: ${versionSpec}`);
return versionSpec;
} else if (getEnv("BUF_VERSION")) {
const resolvedVersion = getEnv("BUF_VERSION");
core.info(`Using the version of buf from $BUF_VERSION: ${resolvedVersion}`);
return resolvedVersion;
}
return "";
}
// latestVersion returns the latest version of buf from GitHub releases.
async function latestVersion(
github: InstanceType<typeof GitHub>,
): Promise<string> {
const release = await github.rest.repos.getLatestRelease({
owner: "bufbuild",
repo: "buf",
});
const resolvedVersion = release.data.tag_name.replace(/^v/, "");
core.info(`Using the latest release of buf: ${resolvedVersion}`);
return resolvedVersion;
}
// platformTable is a mapping of platform to architecture to executable URL.
// The platform and architecture are based on the Node.js process properties.
// The available platforms can be found at:
// - https://nodejs.org/api/process.html#process_process_platform
// - https://nodejs.org/api/process.html#process_process_arch
type platformTable = {
[platform in NodeJS.Platform]?: {
[arch in NodeJS.Architecture]?: string;
};
};
// downloadBuf downloads the buf binary and returns the path to the binary.
async function downloadBuf(
version: string,
githubToken: string,
): Promise<string> {
const table: platformTable = {
darwin: {
x64: "buf-Darwin-x86_64",
arm64: "buf-Darwin-arm64",
},
linux: {
x64: "buf-Linux-x86_64",
arm64: "buf-Linux-aarch64",
},
win32: {
x64: "buf-Windows-x86_64.exe",
arm64: "buf-Windows-arm64.exe",
},
};
const platform = table[process.platform];
if (!platform) {
throw new Error(
`The "${process.platform}" platform is not supported with a Buf release.`,
);
}
const executable = platform[process.arch];
if (!executable) {
throw new Error(
`The "${process.arch}" architecture is not supported with a Buf release.`,
);
}
const downloadURL = `https://github.com/bufbuild/buf/releases/download/v${version}/${executable}`;
const auth = githubToken ? `token ${githubToken}` : undefined;
if (!auth) {
// Warn if the GitHub token is not set. Don't fail as not required.
core.warning(
"Downloading buf without a GitHub API token, rate limits may apply.",
);
}
try {
return await tc.downloadTool(downloadURL, undefined, auth);
} catch (error) {
throw new Error(
`Failed to download buf version ${version} from "${downloadURL}": ${error}`,
);
}
}
+488
View File
@@ -0,0 +1,488 @@
// Copyright 2024-2025 Buf Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import * as core from "@actions/core";
import * as exec from "@actions/exec";
import { GitHub } from "@actions/github/lib/utils";
import { context, getOctokit } from "@actions/github";
import { create } from "@bufbuild/protobuf";
import { createConnectTransport } from "@connectrpc/connect-web";
import { createClient, ConnectError, Code } from "@connectrpc/connect";
import { LabelService } from "@buf/bufbuild_registry.bufbuild_es/buf/registry/module/v1/label_service_pb";
import { LabelRefSchema } from "@buf/bufbuild_registry.bufbuild_es/buf/registry/module/v1/label_pb";
import * as parseDiff from "parse-diff";
import { getInputs, Inputs, getEnv } from "./inputs";
import { Outputs } from "./outputs";
import { installBuf } from "./installer";
import { findCommentOnPR, commentOnPR } from "./comment";
import { parseModuleNames, ModuleName } from "./config";
// URL for the public GitHub API.
const publicGitHubApiUrl = "https://api.github.com";
// main is the entrypoint for the action.
async function main() {
const inputs = getInputs();
const github = getOctokit(inputs.github_token);
// Check if the action is running on a GitHub Enterprise instance.
// If so, use the public GitHub API for resolving the Buf version etc.
let publicGithubToken = inputs.github_token;
let publicGithub: InstanceType<typeof GitHub> | undefined = github;
const apiUrl = process.env.GITHUB_API_URL || ``;
if (!apiUrl.startsWith(publicGitHubApiUrl)) {
core.info("Running on GitHub Enterprise, using public GitHub API.");
publicGithubToken = inputs.public_github_token;
if (publicGithubToken == "") {
publicGithub = undefined;
} else {
publicGithub = getOctokit(publicGithubToken, {
baseUrl: publicGitHubApiUrl,
});
}
}
const [bufPath, bufVersion] = await installBuf(
publicGithub,
publicGithubToken,
inputs.version,
);
core.setOutput(Outputs.BufVersion, bufVersion);
core.setOutput(Outputs.BufPath, bufPath);
core.saveState(Outputs.BufPath, bufPath);
if (inputs.github_actor == "dependabot[bot]") {
core.info("Skipping steps for dependabot");
return;
}
await login(bufPath, inputs);
if (inputs.setup_only) {
core.info("Setup only, skipping steps");
return;
}
// Parse the module names from the input.
const moduleNames = await parseModuleNames(bufPath, inputs.input);
// Run the buf workflow.
const steps = await runWorkflow(bufPath, inputs, moduleNames);
// Create a summary of the steps.
const summary = createSummary(inputs, steps, moduleNames);
// Comment on the PR with the summary, if requested.
if (inputs.pr_comment) {
const commentID = await findCommentOnPR(context, github);
await commentOnPR(
context,
github,
commentID,
`The latest Buf updates on your PR. Results from workflow <a href="${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}">${context.workflow} / ${context.job} (pull_request)</a>.\n\n${summary.stringify()}`,
);
}
// Write the summary to a file defined by GITHUB_STEP_SUMMARY.
// NB: Write empties the buffer and must be after the comment.
await summary.write();
// Finally, set the status of the action.
for (const [key, value] of Object.entries(steps) as [
keyof Steps,
Result | undefined,
][]) {
if (value?.status == Status.Failed) {
core.setFailed(`Failed ${key}`);
}
}
}
main()
.catch((err) => core.setFailed(err.message))
.then(() => core.debug(`done in ${process.uptime()} s`));
// Steps is the result of each step in the workflow. Each step is optional.
// Steps are run in order, buy may be batched together if independent.
interface Steps {
build?: Result;
lint?: Result;
format?: Result;
breaking?: Result;
push?: Result;
archive?: Result;
}
// createSummary creates a GitHub summary of the steps. The summary is a table
// with the name and status of each step.
function createSummary(
inputs: Inputs,
steps: Steps,
moduleNames: ModuleName[],
): typeof core.summary {
const table = [
[
{ data: "Build", header: true },
{ data: "Format", header: true },
{ data: "Lint", header: true },
{ data: "Breaking", header: true },
{ data: "Updated (UTC)", header: true },
],
[
message(steps.build),
message(steps.format),
message(steps.lint),
message(steps.breaking),
new Date().toLocaleString("en-US", {
day: "numeric",
month: "short",
year: "numeric",
hour: "numeric",
minute: "numeric",
hour12: true,
timeZone: "UTC",
}),
],
];
// If push or archive is enabled add a link to the registry.
let output = core.summary.addTable(table);
if (inputs.push && moduleNames.length > 0) {
const modules = moduleNames.map(
(moduleName) =>
`<a href="https://${moduleName.name}">${moduleName.name}</a>`,
);
output = output.addRaw(`Pushed to ${modules.join(", ")}.`, true);
}
if (inputs.archive && moduleNames.length > 0) {
const modules = moduleNames.map(
(moduleName) =>
`<a href="https://${moduleName.name}">${moduleName.name}</a>`,
);
output = output.addRaw(
`Archived labels ${inputs.archive_labels.join(", ")} to ${modules.join(", ")}.`,
true,
);
}
return output;
}
// runWorkflow runs the buf workflow. It returns the results of each step.
// First, it builds the input. If the build fails, the workflow stops.
// Next, it runs lint, format, and breaking checks. If any of these fail, the workflow stops.
// Finally, it pushes or archives the label to the registry.
async function runWorkflow(
bufPath: string,
inputs: Inputs,
moduleNames: ModuleName[],
): Promise<Steps> {
const steps: Steps = {};
steps.build = await build(bufPath, inputs);
if (steps.build.status == Status.Failed) {
if (steps.build.stderr.match(/had no .proto files/)) {
core.info(
'Did you forget to add the "actions/checkout@v4" checkout step to your workflow?',
);
}
return steps;
}
const checks = await Promise.all([
lint(bufPath, inputs),
format(bufPath, inputs),
breaking(bufPath, inputs),
]);
steps.lint = checks[0];
steps.format = checks[1];
steps.breaking = checks[2];
if (checks.some((result) => result.status == Status.Failed)) {
return steps;
}
steps.push = await push(bufPath, inputs, moduleNames);
steps.archive = await archive(inputs, moduleNames);
return steps;
}
// login logs in to the Buf registry, storing credentials.
async function login(bufPath: string, inputs: Inputs) {
const { token, domain } = inputs;
if (token == "") {
core.debug("Skipping login, no token provided");
return;
}
core.debug(`Logging in to ${domain}`);
await exec.exec(bufPath, ["registry", "login", domain, "--token-stdin"], {
input: Buffer.from(token + "\n"),
});
}
// build runs the "buf build" step.
async function build(bufPath: string, inputs: Inputs): Promise<Result> {
const args = ["build", "--error-format", "github-actions"];
if (inputs.input) {
args.push(inputs.input);
}
for (const path of inputs.paths) {
args.push("--path", path);
}
for (const path of inputs.exclude_paths) {
args.push("--exclude-path", path);
}
if (inputs.exclude_imports) {
args.push("--exclude-imports");
}
return run(bufPath, args);
}
// lint runs the "buf lint" step.
async function lint(bufPath: string, inputs: Inputs): Promise<Result> {
if (!inputs.lint) {
core.debug("Skipping lint");
return skip();
}
const args = ["lint", "--error-format", "github-actions"];
if (inputs.input) {
args.push(inputs.input);
}
for (const path of inputs.paths) {
args.push("--path", path);
}
for (const path of inputs.exclude_paths) {
args.push("--exclude-path", path);
}
return run(bufPath, args);
}
// format runs the "buf format" step.
async function format(bufPath: string, inputs: Inputs): Promise<Result> {
if (!inputs.format) {
core.debug("Skipping format");
return skip();
}
const args = [
"format",
"--diff",
"--error-format",
"github-actions",
"--exit-code",
];
if (inputs.input) {
args.push(inputs.input);
}
for (const path of inputs.paths) {
args.push("--path", path);
}
for (const path of inputs.exclude_paths) {
args.push("--exclude-path", path);
}
const result = await run(bufPath, args);
if (result.status == Status.Failed && result.stdout.startsWith("diff")) {
// If the format step fails, parse the diff and write github annotations.
const diff = parseDiff(result.stdout);
result.stdout = ""; // Clear the stdout to count the number of changes.
for (const file of diff) {
result.stdout += `::error file=${file.to}::Format diff -${file.deletions}/+${file.additions}.\n`;
}
console.log(result.stdout);
}
return result;
}
// breaking runs the "buf breaking" step.
async function breaking(bufPath: string, inputs: Inputs): Promise<Result> {
if (!inputs.breaking) {
core.debug("Skipping breaking");
return skip();
}
const args = [
"breaking",
"--error-format",
"github-actions",
"--against",
inputs.breaking_against,
];
if (inputs.input) {
args.push(inputs.input);
}
for (const path of inputs.paths) {
args.push("--path", path);
}
for (const path of inputs.exclude_paths) {
args.push("--exclude-path", path);
}
if (inputs.exclude_imports) {
args.push("--exclude-imports");
}
return run(bufPath, args);
}
// push runs the "buf push" step.
async function push(
bufPath: string,
inputs: Inputs,
moduleNames: ModuleName[],
): Promise<Result> {
if (!inputs.push) {
core.debug("Skipping push");
return skip();
}
if (moduleNames.length == 0) {
core.debug("Skipping push, no named modules detected");
return skip();
}
// We want push to succeed without additional user configuration even if the action is being run on an
// enterprise GitHub instance. Because enterprise GitHub instances can have an arbitrary URL, the Buf CLI with not be
// able to automatically detect the source control url, so we set it explicitly.
const sourceControlUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/commit/${context.sha}`;
const args = [
"push",
"--error-format",
"github-actions",
"--exclude-unnamed",
"--git-metadata",
"--source-control-url",
sourceControlUrl,
];
if (!inputs.push_disable_create) {
args.push("--create");
}
if (inputs.input) {
args.push(inputs.input);
}
return run(bufPath, args);
}
// archive runs the "buf archive" step.
async function archive(
inputs: Inputs,
moduleNames: ModuleName[],
): Promise<Result> {
if (!inputs.archive) {
core.debug("Skipping archive");
return skip();
}
if (moduleNames.length == 0) {
core.debug("Skipping archive, no named modules detected");
return skip();
}
if (inputs.archive_labels.length == 0) {
core.debug("Skipping archive, no labels provided");
return skip();
}
for (const moduleName of moduleNames) {
const baseURL = `https://${moduleName.registry}`;
const transport = createConnectTransport({
baseUrl: baseURL,
});
const client = createClient(LabelService, transport);
for (const label of inputs.archive_labels) {
const labelRef = create(LabelRefSchema, {
value: {
case: "name",
value: {
owner: moduleName.owner,
module: moduleName.module,
label: label,
},
},
});
try {
await client.archiveLabels(
{ labelRefs: [labelRef] },
{
headers: {
Authorization: `Bearer ${inputs.token}`,
},
},
);
core.info(`Archived label ${label} for ${moduleName.name}`);
} catch (err) {
const connectError = ConnectError.from(err);
if (connectError.code == Code.NotFound) {
core.info(
`Skipping archive, label ${label} not found for ${moduleName.name}`,
);
continue;
}
core.error(
`Failed to archive label ${label} for ${moduleName.name}: ${connectError.message}`,
);
return {
status: Status.Failed,
exitCode: 1,
stdout: "",
stderr: connectError.message,
};
}
}
}
return pass();
}
// Status is the status of a command execution.
enum Status {
// Unknown is the default status.
Unknown = 0,
// Passed is the status of a successful command execution.
Passed,
// Failed is the status of a failed command execution.
Failed,
// Skipped is the status of a skipped command execution.
Skipped,
}
// Result is the result of a command execution.
interface Result extends exec.ExecOutput {
status: Status;
}
// run executes the buf command with the given arguments.
async function run(bufPath: string, args: string[]): Promise<Result> {
if (core.isDebug()) {
args = ["--debug", ...args];
}
return exec
.getExecOutput(bufPath, args, {
ignoreReturnCode: true,
env: {
...process.env,
// See: https://buf.build/docs/bsr/authentication
BUF_TOKEN: core.getInput("token") || getEnv("BUF_TOKEN"),
// See: https://buf.build/docs/reference/inputs#https
BUF_INPUT_HTTPS_USERNAME:
getEnv("BUF_INPUT_HTTPS_USERNAME") || core.getInput("github_actor"),
BUF_INPUT_HTTPS_PASSWORD:
getEnv("BUF_INPUT_HTTPS_PASSWORD") || core.getInput("github_token"),
},
})
.then((output) => ({
...output,
status: output.exitCode == 0 ? Status.Passed : Status.Failed,
}));
}
// skip returns a skipped result.
function skip(): Result {
return { status: Status.Skipped, exitCode: 0, stdout: "", stderr: "" };
}
// pass returns a successful result.
function pass(): Result {
return { status: Status.Passed, exitCode: 0, stdout: "", stderr: "" };
}
// message returns a human-readable message for the status. An undefined status
// is considered cancelled.
function message(result: Result | undefined): string {
switch (result?.status) {
case Status.Passed:
return "<code>✅ passed</code>";
case Status.Failed:
return `<code>❌ failed (${result.stdout.split("\n").length - 1})</code>`;
case Status.Skipped:
return "<code>⏩ skipped</code>";
default:
return "<code>🚫 cancelled</code>";
}
}
+19
View File
@@ -0,0 +1,19 @@
// Copyright 2024-2025 Buf Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Outputs are the outputs of the action, matching the outputs in the action.yml.
export enum Outputs {
BufVersion = "buf_version",
BufPath = "buf_path",
}
+37
View File
@@ -0,0 +1,37 @@
// Copyright 2024-2025 Buf Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import * as core from "@actions/core";
import * as exec from "@actions/exec";
import { getInputs, Inputs } from "./inputs";
import { Outputs } from "./outputs";
// main is the entrypoint for the action's post step.
async function main() {
const inputs = getInputs();
const bufPath = core.getState(Outputs.BufPath);
await logout(bufPath, inputs);
}
main()
.catch((err) => core.setFailed(err.message))
.then(() => core.debug(`done in ${process.uptime()} s`));
// logout logs out of the Buf registry, removing credentials.
async function logout(bufPath: string, inputs: Inputs) {
const { domain } = inputs;
core.debug(`Logging out of ${domain}`);
await exec.exec(bufPath, ["registry", "logout", domain]);
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 159 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 128 KiB

+10
View File
@@ -0,0 +1,10 @@
{
"compilerOptions": {
"target": "es2022",
"module": "esnext",
"moduleResolution": "node",
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true
}
}