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
12 changes: 11 additions & 1 deletion .github/workflows/pr-checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -108,12 +108,22 @@ jobs:
# another platform is never compiled by it. The session lock is the
# first such seam, and a Windows-only file that stops compiling would
# otherwise reach a release unnoticed.
#
# This list is every target .goreleaser.yaml releases, and the two must
# stay equal: a target that is released without being compiled here can
# break in a tag rather than in the pull request that broke it.
if: needs.changes.outputs.shell == 'true'
shell: bash
# ARMv6 is the variant the release builds, and Go defaults to ARMv7. The
# check compiles what ships rather than a variant nobody receives; the
# setting is inert for every other target in the list.
env:
GOARM: '6'
run: |
set -euo pipefail

for target in windows/amd64 darwin/amd64 darwin/arm64; do
for target in linux/amd64 linux/arm64 linux/arm linux/386 \
darwin/amd64 darwin/arm64 windows/amd64 windows/arm64; do
echo "Building for ${target}"
GOOS="${target%%/*}" GOARCH="${target##*/}" go build ./...
GOOS="${target%%/*}" GOARCH="${target##*/}" go vet ./...
Expand Down
165 changes: 165 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
name: Release

# A version tag is the whole trigger. Nothing publishes from a branch, so an
# artifact on the release page always corresponds to a tag someone pushed.
on:
push:
tags:
- 'v*'

# Read-only by default. Only the job that publishes the release is granted write
# access to repository contents, and nothing here is granted anything else.
permissions:
contents: read

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: false

jobs:
# A tag may not ship artifacts that fail the proof. This is the same gate a
# pull request runs and the same script a contributor runs locally, so the
# release is held to the standard the repository already enforces rather than
# to a second, weaker one.
acceptance:
name: Architecture Proof
runs-on: ubuntu-latest
steps:
- name: Check out repository
uses: actions/checkout@v7
with:
persist-credentials: false

- name: Set up Go
uses: actions/setup-go@v7
with:
go-version: '1.25.x'
cache-dependency-path: |
go.mod
sdk/go.mod

- name: Run the architecture-proof acceptance gate
run: ./scripts/acceptance.sh

release:
name: Publish artifacts
needs: acceptance
runs-on: ubuntu-latest
permissions:
# Creating the release and uploading its assets. GoReleaser needs nothing
# further: this release publishes no container image, no Homebrew tap, and
# no package repository.
contents: write
steps:
- name: Check out repository
uses: actions/checkout@v7
with:
# GoReleaser reads the tag history to determine the version and to
# build the changelog, and a shallow clone leaves it with neither.
fetch-depth: 0
persist-credentials: false

- name: Set up Go
uses: actions/setup-go@v7
with:
go-version: '1.25.x'
cache-dependency-path: |
go.mod
sdk/go.mod

- name: Build and publish the release
uses: goreleaser/goreleaser-action@v6
with:
# Pinned in step with the Makefile's own pin, so a contributor running
# `make release-snapshot` builds with the version that publishes.
version: v2.17.1
args: release --clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

# Proves what a user will actually find, by fetching it back from the
# release page rather than inspecting the build output: the checksum file
# describes every archive published, the archive named for this tag
# extracts to a binary reporting this tag's version, and the protocol
# version it claims is the one the shell's own source defaults to.
#
# A release that ships a binary still reporting the development
# placeholder, or a checksum file that does not cover every archive
# beside it, fails here rather than reaching a user.
- name: Verify the published artifacts
shell: bash
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail

tag="${GITHUB_REF_NAME}"
archive="wso2-cli-${tag}-linux-amd64.tar.gz"

published="$(mktemp -d)"
gh release download "${tag}" --dir "${published}" --repo "${GITHUB_REPOSITORY}"

cd "${published}"

# What the install scripts will trust has to be what was built. A
# checksum file rewritten or truncated between build and publish is
# exactly what this comparison catches.
if ! diff -u "${GITHUB_WORKSPACE}/dist/checksums.txt" checksums.txt; then
echo "::error::the published checksums.txt is not the one that was built"
exit 1
fi

# --ignore-missing exits non-zero when it verifies nothing, so this
# cannot pass on an empty file. What it cannot see is an archive that
# is present but unlisted, so the two sets of names are compared as
# well: a target dropped from the checksum file would otherwise ship
# unverifiable. Names rather than counts, because a duplicated line
# keeps the count right while leaving another archive uncovered.
sha256sum --check --ignore-missing checksums.txt
find . -maxdepth 1 -type f \( -name '*.tar.gz' -o -name '*.zip' \) \
-printf '%f\n' | sort >published-names.txt
awk '{ name = $2; sub(/^\*/, "", name); print name }' checksums.txt | sort >listed-names.txt
if ! diff -u published-names.txt listed-names.txt; then
echo "::error::the published archives and the checksum file do not describe the same set"
exit 1
fi
echo "$(wc -l <published-names.txt) archives published and all verified"

# The state root is redirected so that reading the module inventory
# cannot touch the runner's real home directory.
workdir="$(mktemp -d)"
state="$(mktemp -d)"
export WSO2_HOME="${state}"
tar -xzf "${archive}" -C "${workdir}"

reported="$("${workdir}/wso2" version)"
echo "${reported}"

if ! grep -qF "${tag}" <<<"${reported}"; then
echo "::error::the released binary reports no version matching ${tag}"
exit 1
fi
if grep -qF '0.0.0-dev' <<<"${reported}"; then
echo "::error::the released binary reports the development placeholder"
exit 1
fi

# The protocol version is injected by .goreleaser.yaml and defaulted in
# internal/version. A release that disagrees with the shell's own
# default would ship a binary claiming support it was not built with,
# so the two are compared rather than assumed to have been kept in step.
#
# Both are required to be non-empty: if the version output ever stops
# carrying a Protocol field under this label, an unguarded comparison
# would find two empty strings equal and report agreement it never saw.
released_protocol="$(awk '$1 == "Protocol" { print $2 }' <<<"${reported}")"
source_protocol="$(cd "${GITHUB_WORKSPACE}" && go run ./cmd/wso2 version | awk '$1 == "Protocol" { print $2 }')"
if [[ -z "${released_protocol}" || -z "${source_protocol}" ]]; then
echo "::error::no protocol version was reported, so nothing was compared"
exit 1
fi
if [[ "${released_protocol}" != "${source_protocol}" ]]; then
echo "::error::released protocol ${released_protocol} does not match the source default ${source_protocol}"
exit 1
fi
echo "Protocol ${released_protocol} matches the source default"
110 changes: 110 additions & 0 deletions .goreleaser.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
# Release configuration for the wso2 shell.
#
# One tagged commit produces every artifact a user can install, plus the
# checksum file the install scripts verify against. The artifact naming
# convention this file implements is a published contract: the install scripts
# derive a download URL from a resolved tag alone, with no manifest to fetch.
# See docs/reference/release-artifacts.md before changing any name template.
#
# Reproduce a release locally without publishing anything:
#
# make release-snapshot
version: 2

project_name: wso2-cli

builds:
# The shell is the only binary this repository releases. Modules are acquired
# by the shell at runtime and are versioned independently, so they are not
# artifacts of a shell release.
- id: wso2
main: ./cmd/wso2
binary: wso2
env:
- CGO_ENABLED=0
flags:
- -trimpath
ldflags:
- -s -w
# The shell version carries no leading "v": the version package prefixes
# one for display, and its semantic-version parse reads the bare form.
- -X github.com/wso2/wso2-cli/internal/version.shellVersion={{ .Version }}
# Kept in step with the default in internal/version by the release
# workflow, which runs the built binary and fails the release if the two
# disagree. Widening the protocol list means changing both.
- -X github.com/wso2/wso2-cli/internal/version.protocolVersion=1
goos:
- linux
- darwin
- windows
goarch:
- amd64
- arm64
- arm
- "386"
goarm:
- "6"
# The eight surviving pairs are exactly the targets the pull-request
# cross-build check compiles. A target may not be released without also
# being compile-checked on every pull request.
ignore:
- goos: darwin
goarch: arm
- goos: darwin
goarch: "386"
- goos: windows
goarch: arm
- goos: windows
goarch: "386"

archives:
# wso2-cli-v0.1.0-darwin-arm64.zip, wso2-cli-v0.1.0-linux-amd64.tar.gz, and
# so on. The tag appears verbatim so that a script that resolved a tag can
# build this name without transforming it.
- id: wso2
ids:
- wso2
name_template: "{{ .ProjectName }}-{{ .Tag }}-{{ .Os }}-{{ .Arch }}"
formats:
- tar.gz
format_overrides:
- goos: darwin
formats:
- zip
- goos: windows
formats:
- zip
# The binary sits at the archive root rather than inside a version-named
# directory, so an installer extracts and moves one known path.
wrap_in_directory: false
# Apache-2.0 requires the licence and notice to travel with the binary.
files:
- LICENSE
- NOTICE

checksum:
# One file covering every archive, published as a release asset. Both install
# scripts fetch it and verify the archive they downloaded before extracting.
name_template: checksums.txt
algorithm: sha256

snapshot:
version_template: "{{ incpatch .Version }}-snapshot"

changelog:
use: github
sort: asc
filters:
exclude:
- "^docs:"
- "^test:"
- "^chore:"
- Merge pull request

release:
# A tag carrying a prerelease identifier publishes as a prerelease, which is
# what the installers' prerelease channel resolves and what keeps a release
# candidate from becoming every user's default.
prerelease: auto
draft: false
name_template: "{{ .Tag }}"
22 changes: 22 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,12 @@ GOLANGCI_LINT ?= $(shell command -v golangci-lint 2>/dev/null || \

SMOKE_PACKAGE := ./test/smoke/

# GoReleaser builds the release artifacts. It is pinned and run through `go run`
# rather than installed, so a contributor reproducing a release uses the same
# version CI does without adding a tool to their machine. The same version is
# pinned in .github/workflows/release.yml; move both together.
GORELEASER ?= $(GO) run github.com/goreleaser/goreleaser/v2@v2.17.1

# A file describing one deployment, sourced by the live targets when it exists.
#
# Go has no dotenv convention and this module stays lean, so nothing parses this
Expand Down Expand Up @@ -82,6 +88,8 @@ help:
@echo ' make lint Lint the shell, including the build-tagged live runs.'
@echo ' make acceptance Run the full architecture-proof acceptance gate.'
@echo ' make smoke-build Compile the live runs without executing them.'
@echo ' make release-check Validate the release configuration.'
@echo ' make release-snapshot Build every release artifact into dist/, publishing nothing.'
@echo ''
@echo 'Against a real deployment (Asgardeo, Identity Server 7.x, or ThunderID):'
@echo ' make smoke-login Log in and broker one acquisition. Opens a browser.'
Expand Down Expand Up @@ -116,6 +124,20 @@ lint:
acceptance:
./scripts/acceptance.sh

# Builds every release artifact into dist/ and publishes nothing. This is how a
# contributor checks a change to .goreleaser.yaml, and how the artifact names and
# checksums can be inspected without pushing a tag. A snapshot names its archives
# for the most recent tag in the checkout and reports a -snapshot version from the
# binary inside; see docs/reference/release-artifacts.md.
.PHONY: release-snapshot
release-snapshot:
$(GORELEASER) release --snapshot --clean

# Checks .goreleaser.yaml without building anything.
.PHONY: release-check
release-check:
$(GORELEASER) check

# Proves the live runs still compile against the shell they drive. The default
# gate cannot do this for them: the tag that keeps them out of it also keeps
# them from being built by it, so without this target they rot silently.
Expand Down
4 changes: 4 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ requirements or architecture.
## Reference and examples

- [Proposed shell commands](reference/commands.md)
- [Release artifacts](reference/release-artifacts.md) is the naming, checksum,
and version contract between a published release and the programs that
download from it. Unlike the documents beside it, it describes what a release
actually publishes rather than a proposed interface.
- [Authentication context examples](examples/authentication-contexts.md)

These documents illustrate proposed interfaces. They are not evidence that the
Expand Down
Loading