Skip to content

Commit

Permalink
feat: post a pr to bcr on successful release (#97)
Browse files Browse the repository at this point in the history
* feat: post a pr to bcr on successful release

* Change to GH app based auth

Following instructions at https://github.com/peter-evans/create-pull-request/blob/main/docs/concepts-guidelines.md#authenticating-with-github-app-generated-tokens

Co-authored-by: Alex Eagle <[email protected]>
  • Loading branch information
kormide and alexeagle authored May 3, 2022
1 parent 58b2c59 commit fc855df
Show file tree
Hide file tree
Showing 5 changed files with 323 additions and 0 deletions.
78 changes: 78 additions & 0 deletions .github/workflows/bcr.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# GitHub Actions workflow to create a pull request to update the bazel central
# registry after a successful release.
#
# Usage:
# 1. Fork bazelbuild/bazel-central-registry to your org or personal account.
# 2. Set the workflow env variables below (see instructions for each variable).
# 3. Update the following files in .github/workflows/bcr to be relevant for your
# project:
# - presubmit.yml
# - metadata.template.json
# - source.template.json
# See https://docs.bazel.build/versions/main/bzlmod.html#bazel-central-registry
# for more information.
# 4. A PR will be posted from your fork to bazelbuild/bazel-central-registry
# with an updated module entry after a successful release.

name: BCR

on:
release:
types: [published]

env:
# Bazel central registry to post a pull request to.
BCR: aspect-build/bazel-central-registry

# Fork of the bazel central registery to push the pull request branch to
# BCR_FORK: aspect-build/bazel-central-registry
# TODO: disable this for bazel-lib as we always want to push to our own fork,
# but re-enable for later plugin

jobs:
bcr-pull-request:
runs-on: ubuntu-latest
steps:
- uses: actions/setup-node@v2
with:
node-version: "14"
- name: Get the tag
id: get_tag
run: echo ::set-output name=TAG::${GITHUB_REF#refs/tags/}
- name: Checkout this repo to access scripts
uses: actions/checkout@v3
- name: Checkout the released version of this repo
uses: actions/checkout@v3
with:
ref: ${{ env.GITHUB_REF }}
path: project
- name: Checkout bcr
uses: actions/checkout@v3
with:
repository: ${{ env.BCR }}
path: bcr
- uses: tibdex/github-app-token@v1
id: generate-token
with:
app_id: ${{ secrets.BCR_APP_ID }}
private_key: ${{ secrets.BCR_APP_PRIVATE_KEY }}
- name: Create bcr entry
run: node .github/workflows/create-bcr-entry.mjs project bcr $GITHUB_REPOSITORY ${{ steps.get_tag.outputs.TAG }}
- name: Create Pull Request
id: post_pr
uses: peter-evans/create-pull-request@v4
with:
token: ${{ steps.generate-token.outputs.token }}
path: bcr
# push-to-fork: ${{ env.BCR_FORK }}
commit-message: ${{ github.repository }}@${{ steps.get_tag.outputs.TAG }}
branch: ${{ github.repository }}@${{ steps.get_tag.outputs.TAG }}
title: ${{ github.repository }}@${{ steps.get_tag.outputs.TAG }}
body: |
[Automated] Publish ${{ github.repository }}@${{ steps.get_tag.outputs.TAG }} to bcr by [bazel-contrib](https://github.com/bazel-contrib).
Manual checks:
- [ ] I have checked that the version is correct.
- [ ] I have checked that the compatibility_level in MODULE.bazel is correct.
- name: Echo PR url
run: echo ${{ steps.post_pr.outputs.pull-request-url }}
12 changes: 12 additions & 0 deletions .github/workflows/bcr/metadata.template.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"homepage": "https://docs.aspect.dev/bazel-lib",
"maintainers": [
{
"email": "[email protected]",
"github": "aspect-build",
"name": "Aspect team"
}
],
"versions": [],
"yanked_versions": {}
}
14 changes: 14 additions & 0 deletions .github/workflows/bcr/presubmit.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
build_targets: &build_targets
- "@aspect_bazel_lib//lib/tests:expand_template_test"

platforms:
centos7:
build_targets: *build_targets
debian10:
build_targets: *build_targets
macos:
build_targets: *build_targets
ubuntu2004:
build_targets: *build_targets
windows:
build_targets: *build_targets
5 changes: 5 additions & 0 deletions .github/workflows/bcr/source.template.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"integrity": "**leave this alone**",
"strip_prefix": "{REPO}-{VERSION}",
"url": "https://github.com/{OWNER}/{REPO}/archive/{TAG}.tar.gz"
}
214 changes: 214 additions & 0 deletions .github/workflows/create-bcr-entry.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
import crypto from "crypto";
import {
readFileSync,
mkdirSync,
existsSync,
copyFileSync,
writeFileSync,
appendFileSync,
} from "fs";
import https from "https";
import { resolve } from "path";

/**
* Create a bcr entry for a new version of this repository.
*
* Usage: create-bcr-entry [project_path] [bcr_path] [owner_slash_repo] [tag]
*
* project_path: path to the project's repository; should contain a
* root level MODULE.bazel file and a .github/workflows/bcr folder
* with templated bcr entry files.
* bcr_path: path to the bcr repository
* owner_slash_repo: the github owner/repository name of the project
* tag: the github tag for this version, e.g., "v1.0.0" or "1.0.0"
*
*/
async function main(argv) {
if (argv.length !== 4) {
console.error(
"usage: create-bcr-entry [project_path] [bcr_path] [owner_slash_repo] [tag]"
);
process.exit(1);
}

const projectPath = argv[0];
const bcrPath = argv[1];
const ownerSlashRepo = argv[2];
const tag = argv[3];
const version = getVersionFromTag(tag);

const moduleName = getModuleName(resolve(projectPath, "MODULE.bazel"));
const bcrTemplatesPath = resolve(projectPath, ".github/workflows/bcr");
const bcrEntryPath = resolve(bcrPath, "modules", moduleName);
const bcrVersionEntryPath = resolve(bcrEntryPath, version);

if (!existsSync(bcrEntryPath)) {
mkdirSync(bcrEntryPath);
}

updateMetadataFile(
resolve(bcrTemplatesPath, "metadata.template.json"),
resolve(bcrEntryPath, "metadata.json"),
version
);

mkdirSync(bcrVersionEntryPath);

stampModuleFile(
resolve(projectPath, "MODULE.bazel"),
resolve(bcrVersionEntryPath, "MODULE.bazel"),
version
);

await stampSourceFile(
resolve(bcrTemplatesPath, "source.template.json"),
resolve(bcrVersionEntryPath, "source.json"),
ownerSlashRepo,
version,
tag
);

// Copy over the presubmit file
copyFileSync(
resolve(bcrTemplatesPath, "presubmit.yml"),
resolve(bcrVersionEntryPath, "presubmit.yml")
);
}

function getModuleName(modulePath) {
const moduleContent = readFileSyncOrFail(
modulePath,
"Cannot find MODULE.bazel; bzlmod requires this file at the root of your workspace."
);

const regex = /module\(.*?name\s*=\s*"(\w+)"/s;
const match = moduleContent.match(regex);
if (match) {
return match[1];
}
throw new Error("Could not parse module name from module file");
}

function updateMetadataFile(sourcePath, destPath, version) {
let publishedVersions = [];
if (existsSync(destPath)) {
const existingMetadata = JSON.parse(
readFileSync(destPath, { encoding: "utf-8" })
);
publishedVersions = existingMetadata.versions;
}

if (publishedVersions.includes(version)) {
console.error(`Version ${version} is already published to this registry`);
process.exit(1);
}

const metadata = JSON.parse(
readFileSyncOrFail(sourcePath),
`Cannot find metadata template ${sourcePath}; did you forget to create it?`
);
metadata.versions = [...publishedVersions, version];
metadata.versions.sort();

writeFileSync(destPath, JSON.stringify(metadata, null, 4) + "\n");
}

function stampModuleFile(sourcePath, destPath, version) {
const module = readFileSyncOrFail(
sourcePath,
"Cannot find MODULE.bazel; bzlmod requires this file at the root of your workspace."
);

const stampedModule = module.replace(
/(^.*?module\(.*?version\s*=\s*")[\w.]+(".*$)/s,
`$1${version}$2`
);

writeFileSync(destPath, stampedModule, {
encoding: "utf-8",
});
}

async function stampSourceFile(
sourcePath,
destPath,
ownerSlashRepo,
version,
tag
) {
const owner = ownerSlashRepo.substring(0, ownerSlashRepo.indexOf("/"));
const repo = ownerSlashRepo.substring(ownerSlashRepo.indexOf("/") + 1);

// Substitute variables into source.json
const sourceContent = readFileSyncOrFail(
sourcePath,
`Cannot find source template ${sourcePath}; did you forget to create it?`
);
const substituted = sourceContent
.replace(/{REPO}/g, repo)
.replace(/{OWNER}/g, owner)
.replace(/{VERSION}/g, version)
.replace(/{TAG}/g, tag);

// Compute the integrity hash
const sourceJson = JSON.parse(substituted);
const filename = sourceJson.url.substring(
sourceJson.url.lastIndexOf("/") + 1
);

await download(sourceJson.url, filename);

const hash = crypto.createHash("sha256");
hash.update(readFileSync(filename));
const digest = hash.digest("base64");
sourceJson.integrity = `sha256-${digest}`;

writeFileSync(destPath, JSON.stringify(sourceJson, undefined, 4), {
encoding: "utf-8",
});
}

function getVersionFromTag(version) {
if (version.startsWith("v")) {
return version.substring(1);
}
}

function download(url, dest) {
return new Promise((resolve, reject) => {
https
.get(url, (response) => {
response.on("data", (chunk) => {
appendFileSync(dest, chunk);
});
response.on("end", () => {
resolve();
});
})
.on("error", (err) => {
reject(new Error(err.message));
});
});
}

function readFileSyncOrFail(filename, notExistsMsg) {
try {
return readFileSync(filename, { encoding: "utf-8" });
} catch (error) {
if (error.code === "ENOENT") {
console.error(notExistsMsg);
process.exit(1);
}
throw error;
}
}

(async () => {
const argv = process.argv.slice(2);
try {
await main(argv);
} catch (error) {
console.error(error);
process.exit(1);
}
})();

0 comments on commit fc855df

Please sign in to comment.