diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..9847cb2 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,15 @@ +version: 2 +updates: + # Keep SHA-pinned GitHub Actions current. Dependabot bumps the commit SHA and + # the trailing version comment together, so pinning does not go stale. + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + commit-message: + prefix: "ci" + groups: + github-actions: + update-types: + - "minor" + - "patch" diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 27b265b..e05bfea 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -10,11 +10,34 @@ jobs: check: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: actions/setup-go@v5 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + - uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5.6.0 with: go-version-file: go.mod cache: true + # Guard against release-version drift: Release Please is the sole version + # writer, so .release-please-manifest.json and package.json must always + # agree and be valid semver. Catches drift on the PR instead of at + # publish time. + - name: Verify release version consistency + run: | + node -e ' + const fs = require("fs"); + const manifest = JSON.parse(fs.readFileSync(".release-please-manifest.json", "utf8"))["."]; + const pkg = JSON.parse(fs.readFileSync("package.json", "utf8")).version; + const semver = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/; + for (const [name, v] of [[".release-please-manifest.json", manifest], ["package.json", pkg]]) { + if (typeof v !== "string" || !semver.test(v)) { + console.error(name + " version is not valid semver: " + JSON.stringify(v)); + process.exit(1); + } + } + if (manifest !== pkg) { + console.error("Release version drift: .release-please-manifest.json is " + manifest + " but package.json is " + pkg + ". Release Please must be the sole version writer."); + process.exit(1); + } + console.log("Release version consistent: " + pkg); + ' - run: make openapi-generated-check - run: make lint - run: make test @@ -29,8 +52,8 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 30 steps: - - uses: actions/checkout@v4 - - uses: actions/setup-go@v5 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + - uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5.6.0 with: go-version-file: go.mod cache: true diff --git a/.github/workflows/publish-cli.yml b/.github/workflows/publish-cli.yml index 28aa0fb..e38f6f1 100644 --- a/.github/workflows/publish-cli.yml +++ b/.github/workflows/publish-cli.yml @@ -20,7 +20,7 @@ jobs: name: Validate Release Ref runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 with: fetch-depth: 0 @@ -32,10 +32,9 @@ jobs: fi STABLE_SEMVER_RE='^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$' - NIGHTLY_SEMVER_RE='^v0\.0\.[0-9]+-nightly\.[0-9]{8}\.[0-9]+$' - if [[ ! "$GITHUB_REF_NAME" =~ $STABLE_SEMVER_RE ]] && [[ ! "$GITHUB_REF_NAME" =~ $NIGHTLY_SEMVER_RE ]]; then + if [[ ! "$GITHUB_REF_NAME" =~ $STABLE_SEMVER_RE ]]; then echo "Unsupported release tag: ${GITHUB_REF_NAME}" - echo "Release tags must use stable SemVer form vMAJOR.MINOR.PATCH or nightly form v0.0.N-nightly.YYYYMMDD.NUMBER." + echo "Release tags must use stable SemVer form vMAJOR.MINOR.PATCH." exit 1 fi @@ -58,15 +57,17 @@ jobs: prerelease: ${{ steps.release.outputs.prerelease }} latest: ${{ steps.release.outputs.latest }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 with: fetch-depth: 0 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + with: + node-version: "24" - name: Resolve release version id: release run: | set -euo pipefail - NIGHTLY_SEMVER_RE='^v0\.0\.[0-9]+-nightly\.[0-9]{8}\.[0-9]+$' if [ "$GITHUB_REF" = "refs/heads/main" ]; then DATE_PART="$(date -u +%Y%m%d)" @@ -87,13 +88,16 @@ jobs: LATEST="false" else RELEASE_TAG="$GITHUB_REF_NAME" - if [[ "$GITHUB_REF_NAME" =~ $NIGHTLY_SEMVER_RE ]]; then - PRERELEASE="true" - LATEST="false" - else - PRERELEASE="false" - LATEST="auto" + NPM_VERSION="$(node -p "require('./package.json').version")" + + if [ "v${NPM_VERSION}" != "$RELEASE_TAG" ]; then + echo "Release tag ${RELEASE_TAG} does not match package.json version ${NPM_VERSION}." + echo "Stable releases must be created from the Release Please release PR commit." + exit 1 fi + + PRERELEASE="false" + LATEST="auto" fi { @@ -141,12 +145,12 @@ jobs: goarch: amd64 ext: ".exe" steps: - - uses: actions/checkout@v4 - - uses: actions/setup-go@v5 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + - uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5.6.0 with: go-version-file: go.mod cache: true - - uses: sigstore/cosign-installer@v4.1.2 + - uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2 - name: Resolve release build environment env: @@ -188,7 +192,7 @@ jobs: test -s "$BUNDLE_PATH" - name: Upload signed binary artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: volcano-binaries-${{ matrix.target.id }} path: dist/${{ matrix.target.id }} @@ -207,11 +211,11 @@ jobs: RELEASE_PRERELEASE: ${{ needs.resolve-release.outputs.prerelease }} RELEASE_LATEST: ${{ needs.resolve-release.outputs.latest }} steps: - - uses: actions/checkout@v4 - - uses: sigstore/cosign-installer@v4.1.2 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + - uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2 - name: Download signed binary artifacts - uses: actions/download-artifact@v4 + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: pattern: volcano-binaries-* path: _release @@ -389,9 +393,9 @@ jobs: contents: read id-token: write # REQUIRED for npm trusted publishing (OIDC) steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 - - uses: actions/setup-node@v4 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version: "24" registry-url: "https://registry.npmjs.org" @@ -401,24 +405,20 @@ jobs: - name: Upgrade npm run: npm install -g npm@11 - - name: Resolve npm version + - name: Verify npm package version id: npm_version env: CLI_VERSION: ${{ needs.resolve-release.outputs.cli_version }} run: | set -euo pipefail - VERSION="${CLI_VERSION#v}" - if [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then - echo "Refusing to publish non-stable version to npm: ${CLI_VERSION}" + VERSION="$(node -p "require('./package.json').version")" + if [ "v${VERSION}" != "$CLI_VERSION" ]; then + echo "Refusing to publish npm version ${VERSION} for release ${CLI_VERSION}." + echo "package.json must match the stable release tag." exit 1 fi echo "version=${VERSION}" >> "$GITHUB_OUTPUT" - - name: Set package version - run: | - npm version "${{ steps.npm_version.outputs.version }}" \ - --no-git-tag-version --allow-same-version - - name: Verify release assets exist env: VERSION: ${{ steps.npm_version.outputs.version }} @@ -441,13 +441,9 @@ jobs: # OIDC trusted publishing: npm detects the id-token and authenticates # automatically; provenance is generated for public repos. No NPM_TOKEN. # VOLCANO_SKIP_DOWNLOAD stops the postinstall fetching a binary in CI. - # - # BOOTSTRAP: until an npm admin does the first manual publish and sets the - # OIDC trusted publisher (workflow file: publish-cli.yml), this step fails - # with ENEEDAUTH. continue-on-error keeps the release green meanwhile -- - # REMOVE it once trusted publishing is live so real failures surface. + # Trusted publishing is live (the package is published on npm), so a + # publish failure is a real failure and must fail the release. - name: Publish to npm - continue-on-error: true env: VOLCANO_SKIP_DOWNLOAD: "1" - run: npm publish + run: npm publish --provenance --access public diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml new file mode 100644 index 0000000..e91d4d8 --- /dev/null +++ b/.github/workflows/release-please.yml @@ -0,0 +1,65 @@ +name: Release Please + +on: + push: + branches: + - main + workflow_dispatch: + +# Serialize release runs so overlapping pushes don't race the release PR/tag. +concurrency: + group: release-please-${{ github.ref }} + cancel-in-progress: false + +# Least privilege for the default GITHUB_TOKEN: the release work runs on the +# GitHub App token minted below, so the default token needs no write scope. +permissions: + contents: read + +jobs: + release-please: + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + # Mint a short-lived GitHub App installation token. Release Please must + # push tags/releases with a non-default token so publish-cli.yml runs: + # refs created with the default GITHUB_TOKEN do not trigger follow-on + # workflows. A GitHub App token replaces the personal access token + # without depending on an individual's credentials (VOL-367). The app id + # is a non-sensitive repo variable; the PEM private key is a secret. The + # app must be installed on this repo with Contents and Pull requests + # read/write. + - uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 + id: app-token + with: + app-id: ${{ vars.VOLCANO_APP_ID }} + private-key: ${{ secrets.VOLCANO_APP_KEY }} + # Down-scope the minted token to only what Release Please needs + # (contents + pull requests to open/merge the release PR and push + # the tag/release; issues for its release labels), rather than the + # app installation's full grant. + permission-contents: write + permission-pull-requests: write + permission-issues: write + + - uses: googleapis/release-please-action@5c625bfb5d1ff62eadeeb3772007f7f66fdcf071 # v4.4.1 + id: release + with: + token: ${{ steps.app-token.outputs.token }} + config-file: release-please-config.json + manifest-file: .release-please-manifest.json + + # Auto-merge the release PR so it merges as soon as it satisfies branch + # protection (required approval + green checks). Squash is the only + # enabled merge method, so the release lands as one Conventional Commit. + - name: Enable auto-merge for the release PR + if: ${{ steps.release.outputs.prs_created == 'true' }} + env: + GH_TOKEN: ${{ steps.app-token.outputs.token }} + RELEASE_PRS: ${{ steps.release.outputs.prs }} + run: | + set -euo pipefail + jq -r '.[].number' <<< "$RELEASE_PRS" | while read -r number; do + echo "Enabling squash auto-merge for release PR #${number}" + gh pr merge "$number" --repo "$GITHUB_REPOSITORY" --auto --squash + done diff --git a/.release-please-manifest.json b/.release-please-manifest.json new file mode 100644 index 0000000..466df71 --- /dev/null +++ b/.release-please-manifest.json @@ -0,0 +1,3 @@ +{ + ".": "0.1.0" +} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 402a00e..69adbf9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -88,13 +88,29 @@ The publish workflow builds signed binaries for `linux-amd64`, `linux-arm64`, `macos-amd64`, `macos-arm64`, and `windows-amd64`. It publishes stable release assets from SemVer tags. -To cut a stable release, merge the release commit to `main`, tag that commit -with a SemVer tag such as `v1.2.3`, and push the tag. Prerelease and build -metadata tags are not stable release tags. The publish workflow also rejects -stable release tags that are not reachable from `origin/main`. - -Required repository variable for stable release publishing: - +Stable releases are managed by Release Please. Normal feature and fix commits +land on `main` using Conventional Commit messages. Release Please opens or +updates a release PR that bumps `package.json`, updates +`.release-please-manifest.json`, and updates `CHANGELOG.md`. Merge that release +PR to cut a stable release. Release Please then creates the SemVer tag and +GitHub Release, such as `v1.2.3`, and the tag-triggered publish workflow +attaches signed binaries and publishes npm. + +Do not manually bump `package.json` outside a Release Please release PR. The +publish workflow rejects stable tags when `package.json` does not match the tag, +or when the tag is not reachable from `origin/main`. + +Prerelease and build metadata tags are not stable release tags. Nightly builds +remain automated from `main` and publish to the mutable `nightly` GitHub Release; +they are not npm releases and are separate from the stable Release Please flow. + +Required repository secrets and variables for stable release publishing: + +- `VOLCANO_APP_ID` (variable) and `VOLCANO_APP_KEY` (secret): the GitHub App + used by Release Please to mint a short-lived installation token. A non-default + token is required so the release tag/release triggers the publish workflow. + The app must be installed on this repo with Contents and Pull requests + read/write. - `VOLCANO_FIRST_PARTY_DEVICE_CLIENT_ID_PRODUCTION` Release assets include platform binaries, adjacent `.sigstore.json` bundles, diff --git a/package.json b/package.json index 5c4e555..e31df74 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@volcano.dev/cli", - "version": "0.0.5", + "version": "0.1.0", "description": "CLI for Volcano's hosting platform.", "keywords": [ "volcano", diff --git a/release-please-config.json b/release-please-config.json new file mode 100644 index 0000000..d9d7fe3 --- /dev/null +++ b/release-please-config.json @@ -0,0 +1,26 @@ +{ + "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json", + "bootstrap-sha": "0d63cdfd0c686a50511216937610b02f6073c0e8", + "include-component-in-tag": false, + "packages": { + ".": { + "release-type": "node", + "package-name": "@volcano.dev/cli", + "changelog-path": "CHANGELOG.md", + "pull-request-title-pattern": "chore${scope}: release${component} ${version}", + "changelog-sections": [ + { "type": "feat", "section": "Features" }, + { "type": "fix", "section": "Bug Fixes" }, + { "type": "perf", "section": "Performance Improvements" }, + { "type": "revert", "section": "Reverts" }, + { "type": "docs", "section": "Documentation" }, + { "type": "refactor", "section": "Code Refactoring" }, + { "type": "build", "section": "Build System" }, + { "type": "ci", "section": "Continuous Integration" }, + { "type": "test", "section": "Tests", "hidden": true }, + { "type": "style", "section": "Styles", "hidden": true }, + { "type": "chore", "section": "Miscellaneous Chores", "hidden": true } + ] + } + } +}