Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 15 additions & 13 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,10 @@ concurrency:

jobs:
# A release is tagged by release-please the moment its PR merges, so a publish failure
# afterwards leaves a tagged version that was never published. This job runs the same
# command the release job does, ahead of every merge, so that never happens.
# afterwards leaves a tagged version that was never published. This job builds every
# artifact the release job uploads, ahead of every merge, so that never happens. It
# stops at a local repository rather than reaching Maven Central: the upload itself
# cannot be rehearsed, since Central refuses a version it already holds.
#
# The one push it skips is the merge of a release PR: release.yml publishes that exact
# commit for real moments later, and the release PR itself has already run this check.
Expand Down Expand Up @@ -67,15 +69,15 @@ jobs:
key: mill-out-publish-${{ runner.os }}-${{ github.sha }}
restore-keys: mill-out-publish-${{ runner.os }}-

# Also generates the POM, the source jar and the Scaladoc jar, which is most of
# what publishing can break.
# Also generates the POM, the source jar and the Scaladoc jar — Maven Central
# requires all three, and they are most of what publishing can break.
- name: Publish every package
run: ./mill __.publishM2Local --m2RepoPath "$PWD/m2"

# The examples are external consumers of the released artifacts (see the README), so
# they resolve from <https://ucb-substrate.github.io/chippy> like any other project
# and check that what a user of Chippy actually gets is usable. Nothing here depends
# on the job above; the two run in parallel.
# they resolve from Maven Central like any other project and check that what a user of
# Chippy actually gets is usable. Nothing here depends on the job above; the two run in
# parallel.
examples:
runs-on: ubuntu-latest
permissions:
Expand Down Expand Up @@ -124,15 +126,15 @@ jobs:
restore-keys: mill-out-examples-${{ runner.os }}-

# The one case where the released artifacts cannot be used: a release bumps the
# examples to the version it is about to publish, so that version is not on GitHub
# Pages yet. Build it from source instead — on release-please's own PR, which it
# examples to the version it is about to publish, so that version is not on Maven
# Central yet. Build it from source instead — on release-please's own PR, which it
# always opens from this branch, and on the commit that lands when that PR is
# merged, which is titled `chore(main): release X.Y.Z`. Its `autorelease:` label is
# not used for this: it is attached after the PR is created and flips to
# `autorelease: tagged` once the release exists.
#
# Deliberately conditional rather than a repository that is always available as a
# fallback: a package missing from GitHub Pages has to fail here, not be quietly
# fallback: a package missing from Maven Central has to fail here, not be quietly
# supplied by this build.
- name: Build Chippy from source for a release
if: >-
Expand All @@ -142,9 +144,9 @@ jobs:
git submodule update --init --recursive --depth 1
./mill __.publishM2Local --m2RepoPath "$PWD/m2"
# `central` has to be spelled out because this replaces coursier's default
# repositories rather than adding to them. The examples list GitHub Pages
# themselves, and Mill resolves a build's own repositories first, so the
# released artifacts still win for anything already published.
# repositories rather than adding to them, and it comes first so that anything
# already released still resolves from Maven Central; the local repository is
# only there to supply the one version that is not on Central yet.
echo "COURSIER_REPOSITORIES=ivy2Local|central|file://$PWD/m2" >> "$GITHUB_ENV"

- name: Compile every example
Expand Down
109 changes: 41 additions & 68 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@ on:
workflows: [ci]
types: [completed]
branches: [main]
# Escape hatch: re-publish the version currently in version.txt and finish tagging it.
# Useful if a release run failed partway; both halves are idempotent.
# Escape hatch: publish the version currently in version.txt and finish tagging it.
# Unlike the GitHub Pages repository this replaced, Maven Central refuses a version it
# already holds, so this recovers a run that failed before Central accepted the bundle.
# A run that failed after that point is finished by re-running the tag job alone.
workflow_dispatch:

permissions: {}
Expand All @@ -20,24 +22,18 @@ concurrency:
group: release
cancel-in-progress: false

# The order of these jobs is the point of this workflow: publish, deploy, and only then
# tag. release-please is split into its two halves — maintaining the release PR, and
# creating the tag and GitHub release — so that nothing is tagged until the artifacts it
# refers to are live. A publish failure therefore leaves no tag to clean up, and re-running
# finishes the release rather than duplicating it.
# The order of these jobs is the point of this workflow: publish, and only then tag.
# release-please is split into its two halves — maintaining the release PR, and creating
# the tag and GitHub release — so that nothing is tagged until the artifacts it refers to
# are live. A publish failure therefore leaves no tag to clean up, and re-running finishes
# the release rather than duplicating it.
#
# publish -> deploy -> tag -> release-pr
# publish -> tag -> release-pr
#
# The file lists them in that order too; `release-pr` runs last because tagging first is
# what stops it from re-proposing a release that is already on its way out.
jobs:
# Adds this release's artifacts to the Maven repository served from GitHub Pages.
#
# The repository is cumulative, but a Pages deployment replaces the whole site, so the
# accumulated state has to live somewhere durable between runs. It is kept as a tarball
# attached to the `maven-repo` release, which is a fixed, non-"Latest" release that exists
# solely as an artifact store. Actions artifacts are not usable here: the Pages artifact
# expires after a day by default and can be retained no longer than 90 days on a public repo.
# Uploads this release's packages to Maven Central through the Sonatype Central Portal.
#
# Nothing has been tagged at this point, so the release is identified by the commit
# release-please's PR lands: "chore(main): release X.Y.Z". `release_created` is not
Expand All @@ -50,7 +46,7 @@ jobs:
github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest
permissions:
contents: write
contents: read
steps:
- uses: actions/checkout@v7
with:
Expand All @@ -75,66 +71,43 @@ jobs:
key: mill-${{ runner.os }}-${{ hashFiles('build.mill', 'mill') }}
restore-keys: mill-${{ runner.os }}-

- name: Restore the previously published repository
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
mkdir -p site
if gh release download maven-repo --pattern repo.tar.gz --output repo.tar.gz; then
tar xzf repo.tar.gz -C site
echo "Restored $(find site -name '*.pom' | wc -l) previously published artifacts."
else
echo "No existing repo.tar.gz; starting a fresh repository."
fi

- name: Add this release to the repository
run: ./mill __.publishM2Local --m2RepoPath "$PWD/site"

# Mill's M2 publisher does not emit checksums. Coursier tolerates their absence, but
# Maven and Gradle warn, so generate any that are missing.
- name: Generate checksums
run: |
find site -type f \( -name '*.jar' -o -name '*.pom' \) -print0 |
while IFS= read -r -d '' f; do
[ -f "$f.md5" ] || md5sum "$f" | cut -d' ' -f1 > "$f.md5"
[ -f "$f.sha1" ] || sha1sum "$f" | cut -d' ' -f1 > "$f.sha1"
done

- name: Save the updated repository
# `publishAll` with no `--publishArtifacts` resolves every PublishModule in the
# build, which is exactly the eleven packages and none of the examples — the same
# wildcard property the README asks you not to break by making an example a
# PublishModule.
#
# Naming a bundle sends all of them to Central as one deployment, so it validates
# and releases them together or not at all. A half-published version is not a state
# this repository can be in — every package shares `version.txt` — and one
# deployment is also one release event against Central's publishing limits rather
# than eleven.
#
# Signing uses Mill's built-in PGP worker rather than a gpg binary, so
# MILL_PGP_SECRET_BASE64 is all the runner needs. `shouldRelease` defaults to true,
# which publishes the bundle as soon as Central has validated it instead of leaving
# it sitting in the portal for someone to release by hand.
#
# The await timeout defaults to two minutes, which validating an eleven-package
# bundle can outrun; twenty costs nothing when things go well, since the call
# returns as soon as Central reports a terminal state.
- name: Publish to Maven Central
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
MILL_SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }}
MILL_SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }}
MILL_PGP_SECRET_BASE64: ${{ secrets.PGP_SECRET_BASE64 }}
MILL_PGP_PASSPHRASE: ${{ secrets.PGP_PASSPHRASE }}
run: |
tar czf repo.tar.gz -C site .
gh release view maven-repo >/dev/null 2>&1 || gh release create maven-repo \
--title "Maven repository store" \
--notes "Rolling tarball of the Maven repository served at the GitHub Pages site. Managed by .github/workflows/release.yml — do not delete." \
--latest=false
gh release upload maven-repo repo.tar.gz --clobber

- uses: actions/upload-pages-artifact@v5
with:
path: site

deploy:
needs: [publish]
runs-on: ubuntu-latest
permissions:
pages: write
id-token: write
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- uses: actions/deploy-pages@v5
id: deployment
./mill mill.javalib.SonatypeCentralPublishModule/publishAll \
--bundleName "chippy-$(cat version.txt)" \
--awaitTimeout 1200000

# Tags the release and creates the GitHub release, now that the artifacts it points at
# are actually resolvable. This keys off the merged release PR's `autorelease: pending`
# label rather than off anything in this run, so re-running after a partial failure
# finishes the release instead of creating a second one. On an ordinary push it is
# skipped along with the publish and deploy jobs it depends on.
# skipped along with the publish job it depends on.
tag:
needs: [deploy]
needs: [publish]
runs-on: ubuntu-latest
permissions:
contents: write
Expand Down
62 changes: 44 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,22 @@ Individual packages can be easily compiled, tested, and used in other projects w

## Usage

Chippy releases are published under the `edu.berkeley.cs` organization to a Maven repository hosted
at <https://ucb-substrate.github.io/chippy>. No credentials are required to resolve from it.
Chippy releases are published under the `io.github.ucb-substrate` namespace to
[Maven Central](https://central.sonatype.com/namespace/io.github.ucb-substrate). Mill, sbt and Maven
all resolve from Central out of the box, so there is no repository to add and no credentials to
configure — only the dependency itself.

To include a package in a new project, add the repository and the dependency. If you are using Mill
1.1.2, for example, add the following to your `build.mill` to use the `diplomacy` package:
The namespace is a coordinate, not a package name: the Scala packages are unchanged, so `chippy`
still lives in `edu.berkeley.cs.chippy` and rocket-chip in `freechips.rocketchip`. Nothing in an
`import` moves.

To include a package in a new project, add the dependency. If you are using Mill 1.1.2, for example,
add the following to your `build.mill` to use the `diplomacy` package:

<!-- x-release-please-start-version -->
```scala
def repositories = Seq("https://ucb-substrate.github.io/chippy")

def mvnDeps = Seq(
mvn"edu.berkeley.cs::diplomacy:0.1.1",
mvn"io.github.ucb-substrate::diplomacy:0.1.1",
)
```
<!-- x-release-please-end -->
Expand All @@ -26,8 +30,7 @@ The equivalent for sbt:

<!-- x-release-please-start-version -->
```scala
resolvers += "chippy" at "https://ucb-substrate.github.io/chippy"
libraryDependencies += "edu.berkeley.cs" %% "diplomacy" % "0.1.1"
libraryDependencies += "io.github.ucb-substrate" %% "diplomacy" % "0.1.1"
```
<!-- x-release-please-end -->

Expand Down Expand Up @@ -56,22 +59,43 @@ The flow is:
(`chore:`, `docs:`, ...) do not trigger a release. Since the repository squash-merges, the **PR
title** is what ends up in the commit history and therefore what release-please parses.
2. release-please keeps a `chore(main): release X.Y.Z` PR open with the pending version bump.
3. Merging that PR tags the release and publishes all packages.
3. Merging that PR publishes every package to Maven Central and then tags the release.

### Publishing credentials

Every package goes to Central as a single signed bundle, uploaded by
`mill.javalib.SonatypeCentralPublishModule/publishAll`, so a version is either published whole or not
at all. Unlike the GitHub Pages repository this replaced, that needs four repository secrets, which
`.github/workflows/release.yml` passes to Mill under its own `MILL_`-prefixed names:

- `SONATYPE_USERNAME` and `SONATYPE_PASSWORD` — a *user token*, generated from the account page of
the [Central Portal](https://central.sonatype.com/). These are not the portal login itself.
- `PGP_SECRET_BASE64` and `PGP_PASSPHRASE` — the base64-encoded private key the artifacts are signed
with, and its passphrase. Central rejects unsigned uploads, and the matching public key has to be
on a public keyserver. `./mill mill.javalib.SonatypeCentralPublishModule/initGpgKeys` generates a
key pair, publishes the public half and prints both values ready to paste in.

The `io.github.ucb-substrate` namespace also has to be verified against the account doing the
publishing before its first upload. Because `ucb-substrate` is a GitHub organization rather than a
personal account, Central does not grant it automatically on login: add the namespace in the portal,
then create a public repository in the organization named after the verification key it hands back.
Everything else still runs with the built-in `GITHUB_TOKEN`.

`.github/workflows/release.yml` runs every step with the built-in `GITHUB_TOKEN`, so no additional
secrets are required.
Central refuses a version it already holds, so a release cannot be re-published over itself. The
`workflow_dispatch` escape hatch therefore recovers a run that failed *before* the bundle was
accepted; one that failed after that is finished by re-running the `tag` job alone.

### Examples

The projects under `examples/` are consumers, not published packages. Each is its own nested build
that depends on Chippy the way an external project would — by released coordinate, resolved from the
public repository — rather than via `moduleDeps`. None of them extends `PublishModule`, which is what
keeps them out of the `__.publishM2Local` wildcard the release job uses. **Do not make an example a
that depends on Chippy the way an external project would — by released coordinate, resolved from
Maven Central — rather than via `moduleDeps`. None of them extends `PublishModule`, which is what
keeps them out of the wildcards the release and ci jobs resolve. **Do not make an example a
`PublishModule`**: it would be picked up by the release, and because its dependencies are the
artifacts that same job is producing, the build would fail to resolve them on a clean checkout.

Their pinned versions are bumped automatically. Every `edu.berkeley.cs` dependency line carries an
`x-release-please-version` comment, and the snippets in this README are wrapped in the block form of
Their pinned versions are bumped automatically. Every `io.github.ucb-substrate` dependency line
carries an `x-release-please-version` comment, and the snippets in this README are wrapped in the block form of
the same annotation, so the release PR updates them alongside `version.txt`. The build files use the
per-line form rather than the block form on purpose: a block rewrites every semver-looking literal it
spans, which would also catch neighbouring lines such as the ScalaTest dependency.
Expand All @@ -81,4 +105,6 @@ in prose — an unmatched opening marker turns the rest of the file into a repla

Because the release PR bumps the examples to the version it is about to publish, the examples briefly
reference a version that does not exist yet — from the moment the release PR is opened until the
publish job finishes after it is merged.
publish job finishes after it is merged. Central takes a few more minutes to propagate a published
bundle to the mirrors coursier fetches from, so a run started immediately after a release may extend
that window slightly.
3 changes: 2 additions & 1 deletion build.mill
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
//| mill-version: 1.1.2
//| mill-jvm-version: 21
//| mill-allow-nested-build-mill: true
package build

Expand Down Expand Up @@ -27,7 +28,7 @@ trait ChippyPublishModule extends ChippyModule, PublishModule {

def makePomSettings(description: String) = PomSettings(
description = description,
organization = "edu.berkeley.cs",
organization = "io.github.ucb-substrate",
url = "https://github.com/ucb-substrate/chippy",
licenses = Seq(License.MIT),
versionControl = VersionControl.github("ucb-substrate", "chippy"),
Expand Down
11 changes: 4 additions & 7 deletions examples/mmio-adder/build.mill
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,12 @@ object `package` extends ScalaModule {
def scalaVersion = "2.13.18"
def chiselVersion = "7.8.0"

// Chippy releases are served from a static Maven repository on GitHub Pages.
def repositories = Seq("https://ucb-substrate.github.io/chippy")

def mvnDeps = Seq(
mvn"org.chipsalliance::chisel:${chiselVersion}",
mvn"edu.berkeley.cs::diplomacy:0.1.1", // x-release-please-version
mvn"edu.berkeley.cs::rocketchip:0.1.1", // x-release-please-version
mvn"edu.berkeley.cs::chippy:0.1.1", // x-release-please-version
mvn"edu.berkeley.cs::testchipip:0.1.1" // x-release-please-version
mvn"io.github.ucb-substrate::diplomacy:0.1.1", // x-release-please-version
mvn"io.github.ucb-substrate::rocketchip:0.1.1", // x-release-please-version
mvn"io.github.ucb-substrate::chippy:0.1.1", // x-release-please-version
mvn"io.github.ucb-substrate::testchipip:0.1.1" // x-release-please-version
)
def scalacPluginMvnDeps = Seq(
mvn"org.chipsalliance:::chisel-plugin:${chiselVersion}",
Expand Down
Loading