diff --git a/.github/actions/start-tsp/action.yaml b/.github/actions/start-tsp/action.yaml index 37f151f..f9d1a6e 100644 --- a/.github/actions/start-tsp/action.yaml +++ b/.github/actions/start-tsp/action.yaml @@ -1,3 +1,4 @@ +# tag-version: v1 # These reusable steps start a TSP docker image in the background and wait for it to report healthy # By default it will start the latest (highest semantic version) TSP from the dev artifact repository. # This can be overridden with a PR comment in the following form specifying the full GAR image name/tag: diff --git a/.github/bump-version.bump.sh b/.github/bump-version.bump.sh index cdf6957..9ec78d6 100755 --- a/.github/bump-version.bump.sh +++ b/.github/bump-version.bump.sh @@ -64,7 +64,11 @@ echo "release=${BUMP_VERSION_RELEASE_PREFIX}${RELEASEVERS}" >> "$GITHUB_OUTPUT" # Derive a new bumped version from the release version. # Increment the last number in the string. -VERSION="$(echo "${RELEASEVERS}" | gawk '{ start=match($0, /(.*[^0-9])([0-9]+)([^0-9]*)$/, a) ; a[2] += 1 ; printf("%s%s%s", a[1], a[2], a[3]) }')" +if ! [[ ${RELEASEVERS} =~ ^(.*[^0-9])([0-9]+)([^0-9]*)$ ]] ; then + echo "No number to increment in '${RELEASEVERS}'" 1>&2 + exit 1 +fi +VERSION="${BASH_REMATCH[1]}$(( 10#${BASH_REMATCH[2]} + 1 ))${BASH_REMATCH[3]}" # Replace [-.]rc[-.$] with pre. VERSION="$(echo "${VERSION}" | sed -E 's/([-.])rc([-.]|$)/\1pre\2/')" # If no [-.]pre[-.$], then append -pre. diff --git a/.github/move-tags.list.sh b/.github/move-tags.list.sh new file mode 100755 index 0000000..ee2f45f --- /dev/null +++ b/.github/move-tags.list.sh @@ -0,0 +1,98 @@ +#!/bin/bash + +# Lists the floating version tags that need to be created or moved so that every +# reusable workflow's and composite action's declared version tag matches its current +# content. See RELEASING.md and move-tags.yaml for details. +# +# Each reusable workflow and composite action declares its major version in a +# "# tag-version: vN" comment in its yaml file. For each declaration, the tag "NAME-vN" +# is printed on stdout if it doesn't exist, or if the workflow's files (the yaml file +# plus any ".github/NAME.*.sh" helper scripts, or the action's directory) differ +# between the tag and HEAD. +# +# A reusable workflow or action without a usable declaration is a misconfiguration: +# it's reported on stderr and the script exits nonzero, after checking everything. +# This repo's own CI workflows don't need declarations and are skipped silently. +# +# Must be run from the repo root, with HEAD at the commit tags should move to and all +# existing tags fetched. The caller is expected to `git tag -f` and force push each +# printed tag. + +set -euo pipefail + +if [ "$#" -ne 0 ] ; then + echo "Usage: $0" 1>&2 + exit 1 +fi + +TAGS=() +ERRORS=0 + +# Check one workflow or action. Arguments are its name, the yaml file declaring its +# version, and the pathspecs that make up its content. Appends to TAGS and sets ERRORS. +check() { + local name="$1" + local vfile="$2" + shift 2 + + local version + version=$(grep -E -o '^# tag-version: v[0-9]+' "$vfile" | head -1 | grep -E -o '[0-9]+$' || true) + if [ -z "$version" ] ; then + # Only reusable workflows and actions need a version; this repo's own CI doesn't. + if [[ "$vfile" == .github/actions/* ]] || grep -q workflow_call "$vfile" ; then + echo "❌ $name: no '# tag-version: vN' comment in $vfile; it will never be released" 1>&2 + ERRORS=1 + fi + return 0 + fi + + # Never touch a tag older than the newest existing one. That means the comment is + # stale or was decremented, and moving the old tag would break its consumers. + local newest="" + local t n + while IFS= read -r t ; do + n="${t#"$name"-v}" + [[ "$n" =~ ^[0-9]+$ ]] || continue + if [ -z "$newest" ] || [ "$n" -gt "$newest" ] ; then + newest="$n" + fi + done < <(git tag --list "$name-v*") + if [ -n "$newest" ] && [ "$newest" -gt "$version" ] ; then + echo "❌ $name: tag $name-v$newest exists but $vfile declares v$version; fix the comment" 1>&2 + ERRORS=1 + return 0 + fi + + local tag="$name-v$version" + if ! git rev-parse -q --verify "refs/tags/$tag" > /dev/null ; then + TAGS+=("$tag") + elif ! git diff --quiet "refs/tags/$tag" HEAD -- "$@" ; then + TAGS+=("$tag") + fi +} + +check_all() { + local f d base name vfile + for f in .github/workflows/*.yaml .github/workflows/*.yml ; do + [ -f "$f" ] || continue + base=$(basename "$f") + name="${base%.*}" + check "$name" "$f" "$f" ".github/$name.*.sh" + done + for d in .github/actions/*/ ; do + [ -d "$d" ] || continue + name=$(basename "$d") + vfile="" + for f in "$d"action.yaml "$d"action.yml ; do + [ -f "$f" ] && vfile="$f" + done + [ -z "$vfile" ] && continue + check "$name" "$vfile" "$d" + done +} + +check_all +if [ "${#TAGS[@]}" -gt 0 ] ; then + printf '%s\n' "${TAGS[@]}" | sort +fi +exit "$ERRORS" diff --git a/.github/spec/move_tags_spec.sh b/.github/spec/move_tags_spec.sh new file mode 100644 index 0000000..0bc3b31 --- /dev/null +++ b/.github/spec/move_tags_spec.sh @@ -0,0 +1,203 @@ +Describe 'move-tags.list.sh' + SCRIPT="$PWD/move-tags.list.sh" + + cleanup() { + if [ -n "${SANDBOX:-}" ] ; then + cd / + rm -rf "$SANDBOX" + fi + } + After 'cleanup' + + # Create a sandbox git repo to run the script in. + setup_repo() { + SANDBOX=$(mktemp -d) + cd "$SANDBOX" || return 1 + export GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_SYSTEM=/dev/null \ + GIT_AUTHOR_NAME=spec GIT_AUTHOR_EMAIL=spec@example.com \ + GIT_COMMITTER_NAME=spec GIT_COMMITTER_EMAIL=spec@example.com + git init -q -b main . + mkdir -p .github/workflows + } + + commit_all() { + git add -A . + git commit -q -m 'commit' + } + + # A minimal reusable workflow declaring the given tag-version. + reusable_workflow() { + printf '# tag-version: %s\non:\n workflow_call:\n' "$1" + } + + new_workflow() { + setup_repo + reusable_workflow v1 > .github/workflows/foo.yaml + commit_all + "$SCRIPT" + } + It 'creates the tag for a new workflow' + When call new_workflow + The output should equal 'foo-v1' + The status should be success + End + + up_to_date_workflow() { + setup_repo + reusable_workflow v1 > .github/workflows/foo.yaml + commit_all + git tag foo-v1 + "$SCRIPT" + } + It 'is quiet when the tag matches the content' + When call up_to_date_workflow + The output should equal '' + The status should be success + End + + changed_workflow() { + setup_repo + reusable_workflow v1 > .github/workflows/foo.yaml + commit_all + git tag foo-v1 + printf '# a change\n' >> .github/workflows/foo.yaml + commit_all + "$SCRIPT" + } + It 'moves the tag when the workflow file changed' + When call changed_workflow + The output should equal 'foo-v1' + The status should be success + End + + unrelated_change() { + setup_repo + reusable_workflow v1 > .github/workflows/foo.yaml + commit_all + git tag foo-v1 + printf 'docs\n' > README.md + commit_all + "$SCRIPT" + } + It 'ignores changes outside the workflow files' + When call unrelated_change + The output should equal '' + The status should be success + End + + changed_helper_script() { + setup_repo + reusable_workflow v1 > .github/workflows/foo.yaml + commit_all + git tag foo-v1 + printf 'echo hi\n' > .github/foo.build.sh + commit_all + "$SCRIPT" + } + It 'moves the tag when a helper script changed' + When call changed_helper_script + The output should equal 'foo-v1' + The status should be success + End + + major_bump() { + setup_repo + reusable_workflow v1 > .github/workflows/foo.yaml + commit_all + git tag foo-v1 + reusable_workflow v2 > .github/workflows/foo.yaml + commit_all + "$SCRIPT" + } + It 'creates the new tag on a major bump, leaving the old one alone' + When call major_bump + The output should equal 'foo-v2' + The status should be success + End + + decremented_version() { + setup_repo + reusable_workflow v2 > .github/workflows/foo.yaml + commit_all + git tag foo-v2 + reusable_workflow v1 > .github/workflows/foo.yaml + commit_all + "$SCRIPT" + } + It 'fails rather than move a tag older than the newest existing one' + When call decremented_version + The output should equal '' + The stderr should not equal '' + The status should be failure + End + + reusable_without_version() { + setup_repo + printf 'on:\n workflow_call:\n' > .github/workflows/foo.yaml + commit_all + "$SCRIPT" + } + It 'fails for a reusable workflow with no tag-version comment' + When call reusable_without_version + The output should equal '' + The stderr should not equal '' + The status should be failure + End + + good_and_bad_workflows() { + setup_repo + reusable_workflow v1 > .github/workflows/foo.yaml + printf 'on:\n workflow_call:\n' > .github/workflows/bad.yaml + commit_all + "$SCRIPT" + } + It 'still lists good tags while failing on a misconfigured workflow' + When call good_and_bad_workflows + The output should equal 'foo-v1' + The stderr should include 'bad' + The status should be failure + End + + repo_ci_without_version() { + setup_repo + printf 'on:\n push:\n' > .github/workflows/repo-ci.yaml + commit_all + "$SCRIPT" + } + It 'silently skips non-reusable workflows with no tag-version comment' + When call repo_ci_without_version + The output should equal '' + The stderr should equal '' + The status should be success + End + + changed_action() { + setup_repo + mkdir -p .github/actions/tsp + printf '# tag-version: v1\nname: tsp\n' > .github/actions/tsp/action.yaml + commit_all + git tag tsp-v1 + printf 'echo hi\n' > .github/actions/tsp/helper.sh + commit_all + "$SCRIPT" + } + It 'moves the tag when any file in a composite action changed' + When call changed_action + The output should equal 'tsp-v1' + The status should be success + End + + multiple_workflows() { + setup_repo + reusable_workflow v1 > .github/workflows/foo.yaml + reusable_workflow v2 > .github/workflows/bar.yaml + commit_all + "$SCRIPT" + } + It 'prints multiple tags sorted' + When call multiple_workflows + The line 1 of output should equal 'bar-v2' + The line 2 of output should equal 'foo-v1' + The status should be success + End +End diff --git a/.github/workflows/bump-version.yaml b/.github/workflows/bump-version.yaml index f99144b..29d3354 100644 --- a/.github/workflows/bump-version.yaml +++ b/.github/workflows/bump-version.yaml @@ -1,3 +1,4 @@ +# tag-version: v1 # This workflow does version management, bumping the patch version on the main branch after a PR is merged. # Requirements: diff --git a/.github/workflows/check-docker-version.yaml b/.github/workflows/check-docker-version.yaml index fae58dd..fd5b29d 100644 --- a/.github/workflows/check-docker-version.yaml +++ b/.github/workflows/check-docker-version.yaml @@ -1,3 +1,4 @@ +# tag-version: v0 # This workflow works with bump-version.yaml to ensure that the version in the # Dockerfile is consistent with what's in the version file. If not, it aborts the # workflow so a human can fix it. This is meant to run on every commit to a PR. diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml index 4296a19..907a210 100644 --- a/.github/workflows/deploy.yaml +++ b/.github/workflows/deploy.yaml @@ -1,3 +1,4 @@ +# tag-version: v1 # This workflow updates files in the deployments repository, to use a new version of a docker image. # 1. Make a PR to update the generic recipe of the application to use the new container image. diff --git a/.github/workflows/docker.yaml b/.github/workflows/docker.yaml index 94a3fac..7fb50bf 100644 --- a/.github/workflows/docker.yaml +++ b/.github/workflows/docker.yaml @@ -1,3 +1,4 @@ +# tag-version: v2 # This workflow builds a docker container, plus more: # - Performs a virus scan. # - Pushes the container to a registry. diff --git a/.github/workflows/get-tsp-ref.yaml b/.github/workflows/get-tsp-ref.yaml index 9c20d32..24e846f 100644 --- a/.github/workflows/get-tsp-ref.yaml +++ b/.github/workflows/get-tsp-ref.yaml @@ -1,3 +1,4 @@ +# tag-version: v1 name: Get TSP ref on: diff --git a/.github/workflows/move-tags.yaml b/.github/workflows/move-tags.yaml new file mode 100644 index 0000000..9404f53 --- /dev/null +++ b/.github/workflows/move-tags.yaml @@ -0,0 +1,112 @@ +# Releases workflow changes by moving floating version tags (e.g. rust-ci-v1) to the +# head of main. Which tags move is decided by move-tags.list.sh, which compares each +# workflow's "# tag-version: vN" comment against the existing tags and their content. +# On pull requests this only reports the tags that merging would move. See RELEASING.md. + +name: Move Workflow Tags + +on: + push: + branches: + - main + pull_request: + workflow_dispatch: + +permissions: + contents: write + pull-requests: write + +jobs: + move-tags: + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v7 + with: + fetch-depth: 0 + - name: List tags to move + id: list + run: | + set -euo pipefail + # Failure is reported at the end of the job, after the tag list and any + # problems have been surfaced in the summary and PR comment. + STATUS=0 + .github/move-tags.list.sh > tags-to-move.txt 2> errors.txt || STATUS=$? + echo "status=$STATUS" >> "$GITHUB_OUTPUT" + cat tags-to-move.txt + cat errors.txt 1>&2 + { + if [ -s errors.txt ] ; then + echo '### Workflow version problems' + echo '```' + cat errors.txt + echo '```' + fi + if [ -s tags-to-move.txt ] ; then + echo '### Workflow tags to move to this commit' + echo '```' + cat tags-to-move.txt + echo '```' + fi + if [ ! -s errors.txt ] && [ ! -s tags-to-move.txt ] ; then + echo 'All workflow tags are up to date.' + fi + } >> "$GITHUB_STEP_SUMMARY" + - name: Report on PR + if: github.event_name == 'pull_request' + env: + GH_TOKEN: ${{ github.token }} + PR: ${{ github.event.pull_request.number }} + run: | + set -euo pipefail + # The marker identifies our comment so we update it instead of adding new ones, + # without disturbing other bot comments (e.g. the code coverage report). + MARKER='' + { + echo "$MARKER" + if [ -s errors.txt ] ; then + echo '❌ This PR has workflow version problems:' + echo '```' + cat errors.txt + echo '```' + fi + if [ -s tags-to-move.txt ] ; then + echo 'Merging this PR will move these workflow tags:' + echo '```' + cat tags-to-move.txt + echo '```' + elif [ ! -s errors.txt ] ; then + echo 'Merging this PR will not move any workflow tags.' + fi + } > comment.txt + post_comment() { + local existing + existing=$(gh api "repos/$GITHUB_REPOSITORY/issues/$PR/comments" --paginate \ + --jq ".[] | select(.body | startswith(\"$MARKER\")) | .id" | head -1) + if [ -n "$existing" ] ; then + gh api -X PATCH "repos/$GITHUB_REPOSITORY/issues/comments/$existing" -F body=@comment.txt > /dev/null + elif [ -s tags-to-move.txt ] || [ -s errors.txt ] ; then + # Only create a comment when there's something to report. + gh api "repos/$GITHUB_REPOSITORY/issues/$PR/comments" -F body=@comment.txt > /dev/null + fi + } + # A read-only token (fork and Dependabot PRs) can't comment; the job summary + # still has the tag list, so don't fail the job over it. + post_comment || echo "::warning::Unable to post the PR comment; see the job summary for the tag list." + - name: Move tags + if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main' && steps.list.outputs.status == '0' + run: | + set -euo pipefail + while IFS= read -r TAG ; do + git tag -f "$TAG" + git push -f origin "refs/tags/$TAG" + echo "Moved $TAG to $(git rev-parse HEAD)" + done < tags-to-move.txt + - name: Fail on workflow version problems + env: + STATUS: ${{ steps.list.outputs.status }} + run: | + set -euo pipefail + if [ "$STATUS" != "0" ] ; then + cat errors.txt 1>&2 + exit 1 + fi diff --git a/.github/workflows/rebuild.yaml b/.github/workflows/rebuild.yaml index 16b24c6..88fe1e1 100644 --- a/.github/workflows/rebuild.yaml +++ b/.github/workflows/rebuild.yaml @@ -1,3 +1,4 @@ +# tag-version: v0 # Periodically rebuild container images by bumping the version. # If being used with `refs`, ensure that the all the branches provided are under # different major versions (for example, docker-jdk v17 and v21). diff --git a/.github/workflows/rust-artifact.yaml b/.github/workflows/rust-artifact.yaml index 7f385d0..13e7765 100644 --- a/.github/workflows/rust-artifact.yaml +++ b/.github/workflows/rust-artifact.yaml @@ -1,3 +1,4 @@ +# tag-version: v0 # Builds the release binary for Rust projects and uploads them as artifacts. # Customization: diff --git a/.github/workflows/rust-cargo-update.yaml b/.github/workflows/rust-cargo-update.yaml index 9438296..0e854af 100644 --- a/.github/workflows/rust-cargo-update.yaml +++ b/.github/workflows/rust-cargo-update.yaml @@ -1,3 +1,4 @@ +# tag-version: v1 # Run `cargo update` and open a PR with the results. name: Rust Cargo Update diff --git a/.github/workflows/rust-ci.yaml b/.github/workflows/rust-ci.yaml index 3a24ca9..ecedf21 100644 --- a/.github/workflows/rust-ci.yaml +++ b/.github/workflows/rust-ci.yaml @@ -1,3 +1,4 @@ +# tag-version: v3 # CI for Rust projects. This compiles (using sccache), runs tests, and checks formatting. # Customization: diff --git a/.github/workflows/rust-daily.yaml b/.github/workflows/rust-daily.yaml index e35f5b4..2c9c83d 100644 --- a/.github/workflows/rust-daily.yaml +++ b/.github/workflows/rust-daily.yaml @@ -1,3 +1,4 @@ +# tag-version: v1 # Daily CI for Rust projects. This runs `cargo check` on all the provided targets/toolchains. # Customization: diff --git a/.github/workflows/rust-release.yaml b/.github/workflows/rust-release.yaml index 77d0dd4..bfa9821 100644 --- a/.github/workflows/rust-release.yaml +++ b/.github/workflows/rust-release.yaml @@ -1,3 +1,4 @@ +# tag-version: v1 # Release to crates.io for Rust projects. name: Rust Release diff --git a/.github/workflows/scala-ci.yaml b/.github/workflows/scala-ci.yaml index 5527153..09e83d8 100644 --- a/.github/workflows/scala-ci.yaml +++ b/.github/workflows/scala-ci.yaml @@ -1,3 +1,4 @@ +# tag-version: v2 # CI for Scala projects. This compiles, runs tests, applies scalafmt + scalafix auto-fixes (pushing them back # to the PR branch when possible), and produces a coverage report. diff --git a/.github/workflows/scala-cve.yaml b/.github/workflows/scala-cve.yaml index f8677e8..362de52 100644 --- a/.github/workflows/scala-cve.yaml +++ b/.github/workflows/scala-cve.yaml @@ -1,3 +1,4 @@ +# tag-version: v1 # Run dependency check for Scala projects. name: Scala CVE diff --git a/.github/workflows/shell-ci.yaml b/.github/workflows/shell-ci.yaml index 0012737..dc75d59 100644 --- a/.github/workflows/shell-ci.yaml +++ b/.github/workflows/shell-ci.yaml @@ -1,3 +1,4 @@ +# tag-version: v0 # Run CI for shell script projects. This runs shellspec. name: Shell CI diff --git a/.github/workflows/sql-smelter.yaml b/.github/workflows/sql-smelter.yaml index 775bbff..92be569 100644 --- a/.github/workflows/sql-smelter.yaml +++ b/.github/workflows/sql-smelter.yaml @@ -1,3 +1,4 @@ +# tag-version: v1 # This workflow writes a comment about which sql-smelter branch to use and uses user provided comments to change it from main if # required. As such, it needs to have a trigger on issue comment created. Something like this: #issue_comment: diff --git a/.github/workflows/typescript-ci.yaml b/.github/workflows/typescript-ci.yaml index 388220e..9814a0a 100644 --- a/.github/workflows/typescript-ci.yaml +++ b/.github/workflows/typescript-ci.yaml @@ -1,3 +1,4 @@ +# tag-version: v1 # CI tasks for Node (TypeScript, JavaScript) repositories. # Customization: diff --git a/.github/workflows/typescript-release.yaml b/.github/workflows/typescript-release.yaml index f30ec77..5c694cd 100644 --- a/.github/workflows/typescript-release.yaml +++ b/.github/workflows/typescript-release.yaml @@ -1,3 +1,4 @@ +# tag-version: v1 # Publish Typescript code to NPM. This will use Trusted Publishing # (https://docs.npmjs.com/trusted-publishers#supported-cicd-providers), so ensure it's set up for the calling repo. diff --git a/.gitignore b/.gitignore index 2f7896d..e08c794 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ target/ +.DS_Store diff --git a/README.md b/README.md index 268688d..bbebdfb 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,7 @@ ## Bootstrapping -To get workflows set up in a repository, follow these steps. Some of these steps aren't required for these workflows, but they're -collected here to make all our repositories consistent. +To get workflows set up in a repository, follow these steps. Some of these steps aren't required for these workflows, but they're collected here to make all our repositories consistent. 1. In the GitHub web UI: 1. Under `General settings` @@ -22,18 +21,14 @@ collected here to make all our repositories consistent. 1. Under `Code security and analysis`, enable all the things. 1. Review the `.github/CODEOWNERS` file. It's unrelated to the workflows, but now's a good chance to check it. - Make sure the any groups appearing in `CODEOWNERS` have been granted at least `Write` access to the repository. -1. Examine existing tags and make sure they're compatible with the new system: It will set tags based on the contents of the - language-specific version file. If necessary, manually adjust versions so there will be no conflicts. -1. Edit the language-specific version file (`package.json`, `Cargo.toml`, `version.sbt`) and increment to the next pre-release - version. (E.g., `1.2.3` -> `1.2.4-pre`) Once your PR is merged, `main` should always be a pre-release. -1. Decide which workflows you're going to use. For a service, it'll likely be: - `bump-version.yaml docker.yaml deploy.yaml` plus a language-specific CI workflow. +1. Examine existing tags and make sure they're compatible with the new system: It will set tags based on the contents of the language-specific version file. If necessary, manually adjust versions so there will be no conflicts. +1. Edit the language-specific version file (`package.json`, `Cargo.toml`, `version.sbt`) and increment to the next pre-release version. (E.g., `1.2.3` -> `1.2.4-pre`) Once your PR is merged, `main` should always be a pre-release. +1. Decide which workflows you're going to use. For a service, it'll likely be: `bump-version.yaml docker.yaml deploy.yaml` plus a language-specific CI workflow. 1. Commit those changes on a branch. Push, PR, and merge. ## Continuous Deployment -These workflows are modular components of a system to manage "deployments" when a PR is merged to `main`. A "deployment" could mean -updating a deployment in Kubernetes, or it could mean publishing a new release of a public repository. +These workflows are modular components of a system to manage "deployments" when a PR is merged to `main`. A "deployment" could mean updating a deployment in Kubernetes, or it could mean publishing a new release of a public repository. 1. bump-version: Bumps the patch version. - Triggered by merge to main. @@ -50,33 +45,19 @@ updating a deployment in Kubernetes, or it could mean publishing a new release o ### Authentication -Several actions require a secret called `WORKFLOW_PAT` to be set to the value -of a personal access token with permissions to operate on various repos. A -personal access token (PAT) has been created for the `leeroy-travis` user in -GitHub, and the value of the token is put into two organization-wide secrets: -the workflow secret WORKFLOW_PAT and also the Dependabot secret WORKFLOW_PAT. +Several actions require a secret called `WORKFLOW_PAT` to be set to the value of a personal access token with permissions to operate on various repos. A personal access token (PAT) has been created for the `leeroy-travis` user in GitHub, and the value of the token is put into two organization-wide secrets: the workflow secret WORKFLOW_PAT and also the Dependabot secret WORKFLOW_PAT. -The PAT has been configured with `workflow` and `admin:org.read:org` -permissions. +The PAT has been configured with `workflow` and `admin:org.read:org` permissions. -To rotate the PAT, log in to leeroy-travis, go to Settings ... Developer -settings ... Personal access tokens ... Tokens (classic). Select the -"org WORKFLOW_PAT" token, then "Regenerate token". Set the expiration to -"never" and save, then copy the value of the token. Then log in as an -account that is an admin of the IronCore Labs GitHub organization, go to -IronCore ... Settings ... Secrets and variables ... Actions". Edit the -"WORKFLOW_PAT" secret, click "enter a new value", and paste the PAT value. +To rotate the PAT, log in to leeroy-travis, go to Settings ... Developer settings ... Personal access tokens ... Tokens (classic). Select the "org WORKFLOW_PAT" token, then "Regenerate token". Set the expiration to "never" and save, then copy the value of the token. Then log in as an account that is an admin of the IronCore Labs GitHub organization, go to IronCore ... Settings ... Secrets and variables ... Actions". Edit the "WORKFLOW_PAT" secret, click "enter a new value", and paste the PAT value. Repeat this process for the "Dependabot" secrets, updating "WORKFLOW_PAT". ## Docker Hub -We have a personal access token (PAT) that we use to authenticate to Docker Hub. Its purpose is to bypass rate limits on pulls -from Docker Hub. It shouldn't be necessary on GitHub-owned runners, because they have a special deal with Docker. But on other -runners it's important. +We have a personal access token (PAT) that we use to authenticate to Docker Hub. Its purpose is to bypass rate limits on pulls from Docker Hub. It shouldn't be necessary on GitHub-owned runners, because they have a special deal with Docker. But on other runners it's important. -The PAT is stored as an organization secret in GitHub, named `DOCKERHUB_TOKEN`. It corresponds to login information kept in -`gdrive/IronCore Engineering/IT_Info/leeroy-travis/docker-hub-info.txt.iron`. +The PAT is stored as an organization secret in GitHub, named `DOCKERHUB_TOKEN`. It corresponds to login information kept in `gdrive/IronCore Engineering/IT_Info/leeroy-travis/docker-hub-info.txt.iron`. To rotate the token: @@ -85,22 +66,8 @@ To rotate the token: 1. Log in to GitHub. 1. Edit the organization secrets for IronCoreLabs, and replace the value of `DOCKERHUB_TOKEN`. -### Workflow Tag Bumping +## Workflow Tag Bumping -After making **non-breaking** changes to GitHub Actions workflows in `.github/workflows`, you can use the `move-workflow-tags.sh` script to get commands to bump the corresponding tags. +Tags are moved automatically when PRs merge to `main`: each reusable workflow declares its major version in a `# tag-version: vN` comment, and the `move-tags.yaml` workflow creates or moves the matching tag. Increment the comment in your PR to release a breaking change as a new major version. See [RELEASING.md](RELEASING.md) for details. -**Usage:** - -```bash -# Bump tags for workflows changed in the last commit -./move-workflow-tags.sh - -# Bump tags for a specific commit -./move-workflow-tags.sh - -# Bump tags for a commit range -./move-workflow-tags.sh .. -``` - -The script will indicate which workflows were changed in the commit(s) and allow you to choose which ones to move the tags for. It will then -display all the commands to run. +Note: a workflow file that doesn't contain `workflow_call` is treated as CI local to this repo — it's excluded from version checking and never gets a tag. The detection in `move-tags.list.sh` is a literal string match, not yaml parsing, so don't mention `workflow_call` in the comments of a local workflow or it will be mistaken for a reusable one and fail the version check. diff --git a/RELEASING.md b/RELEASING.md index c040718..57eee4c 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -1,13 +1,22 @@ # Releasing -Each workflow has its own version (i.e. `rust-ci-v1`, `rebuild-all-docker-v2`). -After you merge a PR to main: +Each reusable workflow and composite action has a floating major version tag (e.g. `rust-ci-v1`, `docker-v2`). Consuming repos pin to these tags, so moving a tag releases the change to every consumer of that major version. -- identify workflow files that were modified in your PR -- create a new tag following semver for that file -- move any major or minor version tags forward as appropriate - 1. `git checkout main && git pull` - 1. `git tag -f foo-v1` - 1. `git push -f origin foo-v1` +## Declaring versions -TODO: build automation that looks at git tags, what files changed, and increments/moves tags for those workflows. Lerna and Pants both have some functionality like this, but most likely something custom to us would be easier for this specific case. Bonus points if the pattern/tool is reusable by others, because this sort of thing is an [open question in the community](https://github.com/orgs/community/discussions/30049) with how new it is. +Every reusable workflow declares its current major version in a comment at the top of its yaml file (composite actions declare it in their `action.yaml`): + +```yaml +# tag-version: v3 +``` + +## What happens on merge + +The `move-tags.yaml` workflow runs on every push to `main` and reconciles tags with content: for each declared version, if the tag `NAME-vN` doesn't exist, or if the workflow's files (its yaml file, its `.github/NAME.*.sh` helper scripts, or the action's directory) differ between the tag and `main`, the tag is created or force-moved to the head of `main`. + +- **Non-breaking change:** leave the `tag-version` comment alone. The current tag moves forward when your PR merges. +- **Breaking change:** increment the `tag-version` comment in the same PR. Merging creates the new tag; the old tag stays where it was, and consumers upgrade by editing their workflow references. + +On pull requests the same workflow runs in report-only mode: its job summary lists the tags that merging will move, so reviewers can check that a breaking change got a new major version. + +Because tags are reconciled against content rather than individual pushes, the automation also catches up on any merges where tags didn't get moved. Misconfigurations fail the check: a reusable workflow or action with no parseable `tag-version` comment (it would never be released), or a declared version older than the newest existing tag (moving the old tag would break its consumers). Problems are reported in the PR comment, and no tags are moved on `main` until they're fixed. diff --git a/move-workflow-tags.sh b/move-workflow-tags.sh deleted file mode 100755 index 82facde..0000000 --- a/move-workflow-tags.sh +++ /dev/null @@ -1,131 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -die() { echo "❌ $*" >&2; exit 1; } - -# Make sure we’re where we expect -repo_root=$(git rev-parse --show-toplevel 2>/dev/null) || die "Not inside a git repo" -cd "$repo_root" - -# Accept a commit or range argument (default HEAD). If it's a commit, -# create a range like COMMIT~1..COMMIT. -COMMIT_ARG="${1:-HEAD}" -if [[ "$COMMIT_ARG" == *..* ]]; then - DIFF_RANGE="$COMMIT_ARG" -else - DIFF_RANGE="${COMMIT_ARG}~1..${COMMIT_ARG}" -fi - -# Find all workflow files under .github/workflows -mapfile -t workflow_files < <(find .github/workflows -maxdepth 1 -type f \( -name '*.yml' -o -name '*.yaml' \) -print 2>/dev/null || true) -(( ${#workflow_files[@]} )) || die "No workflow files found in .github/workflows (*.yml|*.yaml)" - -# Derive workflow names from filenames (e.g. `bump-version.yaml` -> `bump-version`) -workflows=() -for f in "${workflow_files[@]}"; do - base=$(basename "$f") - name="${base%.*}" - workflows+=("$name") -done - -# Get all the tags from the repo -mapfile -t all_tags < <(git tag --list) - -# For each workflow, determine the latest version tag (e.g. select `rust-ci-v1` over `rust-ci-v0`) -declare -A latest_tag -for wf in "${workflows[@]}"; do - best="" - bestn=-1 - for t in "${all_tags[@]}"; do - if [[ "$t" =~ ^${wf}-v([0-9]+)$ ]]; then - n="${BASH_REMATCH[1]}" - if (( n > bestn )); then - bestn="$n" - best="$t" - fi - fi - done - latest_tag["$wf"]="$best" -done - -# Detect changed workflows in the commit range -declare -A changed_map=() -while IFS= read -r path; do - [[ -z "$path" ]] && continue - base=$(basename "$path") - name="${base%.*}" - changed_map["$name"]=1 -done < <(git diff --name-only "$DIFF_RANGE" -- .github/workflows/ 2>/dev/null || true) - -# Detect changes in related helper scripts under .github/ -while IFS= read -r path; do - [[ -z "$path" ]] && continue - base=$(basename "$path") - prefix="${base%%.*}" # e.g. bump-version.bump.sh → bump-version - if [[ " ${workflows[*]} " == *" $prefix "* ]]; then - changed_map["$prefix"]=1 - fi -done < <(git diff --name-only "$DIFF_RANGE" -- .github/ 2>/dev/null || true) - -# Prompt to select which tags to move -echo "Select workflows to move tags for (space-separated indices, or * for all changed) [commit: $COMMIT_ARG]:" -i=1 -for wf in "${workflows[@]}"; do - mark="" - [[ -n "${changed_map[$wf]:-}" ]] && mark="*" - lt="${latest_tag[$wf]}" - [[ -z "$lt" ]] && lt="none" - printf "%2d) %-24s %1s (latest: %s)\n" "$i" "$wf" "$mark" "$lt" - ((i++)) -done -echo "(* = changed in $DIFF_RANGE)" -printf "#? " -read -r indices - -selected=() -# If * is given, include all workflows marked as changed -if [[ "$indices" == "*" ]]; then - for wf in "${workflows[@]}"; do - if [[ -n "${changed_map[$wf]:-}" ]]; then - selected+=("$wf") - fi - done - echo "Selected all changed workflows: ${selected[*]}" -# Otherwise, parse indices and map them to workflows -else - for idx in $indices; do - if [[ "$idx" =~ ^[0-9]+$ ]] && (( idx >= 1 && idx <= ${#workflows[@]} )); then - selected+=("${workflows[$((idx-1))]}") - else - echo "Skipping invalid index: $idx" >&2 - fi - done -fi - -(( ${#selected[@]} )) || die "No workflows selected" - -# Resolve the commit SHA of the target commit -target_sha=$(git rev-parse --verify "$COMMIT_ARG") || die "Invalid commit-ish: $COMMIT_ARG" - -# Output which tags should move and to which SHA -echo -commands=() -for wf in "${selected[@]}"; do - tag="${latest_tag[$wf]}" - if [[ -z "$tag" ]]; then - echo "⚠️ No existing tag for $wf (skipping)" - continue - fi - commands+=("git tag -f $tag $target_sha") - commands+=("git push -f origin $tag") -done - -echo -e "\nProposed tag updates:\n" - -# Print out tag update commands -for cmd in "${commands[@]}"; do - echo "$cmd" -done - -echo -e "\nReminder: this is a helper script that could contain mistakes. Always verify the commands manually for accuracy." -echo "If something goes wrong, you can look at the output from the \`git tag\` commands to see what hash the tag used to point to." diff --git a/src/main.rs b/src/main.rs index e7a11a9..bf1e062 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,24 @@ +// This crate is a hello-world test fixture whose only purpose is to let +// `.github/workflows/rust-ci-test.yaml` exercise the reusable `rust-ci.yaml` +// workflow against this repo. It is not real software. The test below exists +// solely so the `cargo llvm-cov` coverage job reports non-zero line coverage +// instead of posting a confusing "0% coverage" check on every PR. There is +// nothing here worth investigating. + +fn greeting() -> &'static str { + "Hello, world!" +} + fn main() { - println!("Hello, world!"); + println!("{}", greeting()); +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn greeting_is_hello_world() { + assert_eq!(greeting(), "Hello, world!"); + } }