diff --git a/.github/workflows/self-lint.yml b/.github/workflows/self-lint.yml index 096c9e3..78c2cc7 100644 --- a/.github/workflows/self-lint.yml +++ b/.github/workflows/self-lint.yml @@ -3,6 +3,9 @@ on: pull_request: branches: [main] +permissions: + contents: read + jobs: lint: uses: validatedpatterns/github-actions-library/.github/workflows/superlinter.yml@v1 diff --git a/.github/workflows/superlinter.yml b/.github/workflows/superlinter.yml index 6058f7a..2965492 100644 --- a/.github/workflows/superlinter.yml +++ b/.github/workflows/superlinter.yml @@ -7,11 +7,6 @@ on: required: false default: "ubuntu-latest" type: string - sl_version: - description: "github/super-linter version ref" - required: false - default: "slim-v8" - type: string # Free-form env to pass straight to Super-Linter (KEY=VALUE, one per line) # https://thedocumentation.org/super-linter/usage/configuration sl_env: @@ -23,29 +18,32 @@ on: token: required: false +permissions: + contents: read + jobs: superlinter: name: Super-Linter runs-on: ${{ inputs.runner }} - permissions: - contents: read - pull-requests: read steps: - uses: actions/checkout@v5 with: fetch-depth: 0 + persist-credentials: false # Convert the multi-line sl_env into environment variables - name: Apply custom Super-Linter env shell: bash + env: + SL_ENV_INPUT: ${{ inputs.sl_env }} run: | while IFS= read -r line; do [[ -z "$line" || "$line" =~ ^\s*# ]] && continue echo "$line" >> "$GITHUB_ENV" - done <<< "${{ inputs.sl_env }}" + done <<< "$SL_ENV_INPUT" - name: Run Super-Linter - uses: github/super-linter@${{ inputs.sl_version }} + uses: super-linter/super-linter@ffde3b2b33b745cb612d787f669ef9442b1339a6 env: GITHUB_TOKEN: ${{ secrets.token || github.token }} # Any values written to $GITHUB_ENV above are also inherited here diff --git a/README.md b/README.md index 19558e7..1a4e7ef 100644 --- a/README.md +++ b/README.md @@ -13,30 +13,37 @@ Runs [GitHub Super-Linter](https://github.com/super-linter/super-linter) to vali #### Usage **Basic usage:** + ```yaml name: Lint Code Base on: pull_request: branches: [main] +permissions: + contents: read + jobs: lint: uses: validatedpatterns/github-actions-library/.github/workflows/superlinter.yml@v1 ``` **With custom configuration:** + ```yaml name: Lint Code Base on: pull_request: branches: [main] +permissions: + contents: read + jobs: lint: uses: validatedpatterns/github-actions-library/.github/workflows/superlinter.yml@v1 with: runner: ubuntu-22.04 - sl_version: slim-v7 sl_env: | VALIDATE_ALL_CODEBASE=false VALIDATE_MARKDOWN=false @@ -46,11 +53,10 @@ jobs: #### Inputs -| Input | Description | Required | Default | -| ------------ | ------------------------------------------------------------- | -------- | --------------- | -| `runner` | GitHub runner to use | No | `ubuntu-latest` | -| `sl_version` | GitHub Super-Linter version ref | No | `slim-v8` | -| `sl_env` | Extra Super-Linter environment variables (lines of KEY=VALUE) | No | `""` | +| Input | Description | Required | Default | +| -------- | ------------------------------------------------------------- | -------- | --------------- | +| `runner` | GitHub runner to use | No | `ubuntu-latest` | +| `sl_env` | Extra Super-Linter environment variables (lines of KEY=VALUE) | No | `""` | #### Secrets @@ -65,8 +71,8 @@ The workflow supports all Super-Linter configuration options through the `sl_env #### Permissions The workflow requires the following permissions: + - `contents: read` - To checkout the repository -- `pull-requests: read` - To read pull request information These permissions are automatically set by the workflow and don't need to be configured in the calling workflow. @@ -81,4 +87,4 @@ These permissions are automatically set by the workflow and don't need to be con When adding new workflows: 1. Add the workflow YAML file to `.github/workflows/` -2. Update this main README with full documentation including usage, inputs, and examples +2. Update this readme with full documentation including usage, inputs, and examples diff --git a/scripts/tag-release.sh b/scripts/tag-release.sh index 61638ce..6a5452e 100755 --- a/scripts/tag-release.sh +++ b/scripts/tag-release.sh @@ -58,152 +58,161 @@ DRY_RUN="${DRY_RUN:-0}" # Command runner that honors DRY_RUN=1 _run() { - echo "+ $*" - if [[ "$DRY_RUN" != 1 ]]; then - "$@" - fi + echo "+ $*" + if [[ "$DRY_RUN" != 1 ]]; then + "$@" + fi } # --- Helpers --------------------------------------------------------------- # Print an error and exit non-zero. # Usage: die "message" -die() { echo "Error: $*" >&2; exit 1; } +die() { + echo "Error: $*" >&2 + exit 1 +} # Ensure working tree and index are clean. # Exits if there are staged or unstaged changes. require_clean_tree() { - if ! git diff --quiet || ! git diff --cached --quiet; then - die "Working tree not clean. Commit or stash changes first." - fi + if ! git diff --quiet || ! git diff --cached --quiet; then + die "Working tree not clean. Commit or stash changes first." + fi } # Detect the upstream default branch name. # Echoes branch name (e.g., "main"). Dies if remote/branch not found. detect_upstream_branch() { - git remote show "$UPSTREAM_REMOTE" >/dev/null 2>&1 \ - || die "Remote '$UPSTREAM_REMOTE' not found. Set UPSTREAM_REMOTE or add the remote." + git remote show "$UPSTREAM_REMOTE" >/dev/null 2>&1 || + die "Remote '$UPSTREAM_REMOTE' not found. Set UPSTREAM_REMOTE or add the remote." - local head_branch - head_branch="$(git remote show "$UPSTREAM_REMOTE" | awk '/HEAD branch/ {print $NF}')" - [[ -n "$head_branch" ]] || die "Could not detect upstream default branch." - echo "$head_branch" + local head_branch + head_branch="$(git remote show "$UPSTREAM_REMOTE" | awk '/HEAD branch/ {print $NF}')" + [[ -n "$head_branch" ]] || die "Could not detect upstream default branch." + echo "$head_branch" } # Fetch tags and branches from the upstream remote. fetch_upstream() { - _run git fetch "$UPSTREAM_REMOTE" --tags --prune + _run git fetch "$UPSTREAM_REMOTE" --tags --prune } # Pull the upstream default branch (ff-only) if currently checked out. # Usage: pull_upstream_default pull_upstream_default() { - local branch="$1" - local cur_branch - cur_branch="$(git rev-parse --abbrev-ref HEAD)" - if [[ "$cur_branch" == "$branch" ]]; then - _run git pull --ff-only "$UPSTREAM_REMOTE" "$branch" - else - echo "Note: current branch is '$cur_branch' (upstream default is '$branch'). Skipping pull." - fi + local branch="$1" + local cur_branch + cur_branch="$(git rev-parse --abbrev-ref HEAD)" + if [[ "$cur_branch" == "$branch" ]]; then + _run git pull --ff-only "$UPSTREAM_REMOTE" "$branch" + else + echo "Note: current branch is '$cur_branch' (upstream default is '$branch'). Skipping pull." + fi } # Return success if arg looks like a valid moving major tag (vN). is_valid_major_tag() { - [[ "$1" =~ ^v[0-9]+$ ]] + [[ "$1" =~ ^v[0-9]+$ ]] } # Return success if arg looks like a valid semver tag (vN.N.N). is_valid_semver_tag() { - [[ "$1" =~ ^v([0-9]+)\.([0-9]+)\.([0-9]+)$ ]] + [[ "$1" =~ ^v([0-9]+)\.([0-9]+)\.([0-9]+)$ ]] } # Extract the major portion (vN) from a semver tag. # Usage: major_of_semver v1.2.3 → v1 major_of_semver() { - [[ "$1" =~ ^v([0-9]+)\.[0-9]+\.[0-9]+$ ]] && echo "v${BASH_REMATCH[1]}" + [[ "$1" =~ ^v([0-9]+)\.[0-9]+\.[0-9]+$ ]] && echo "v${BASH_REMATCH[1]}" } # Sort semver tags in descending order (vN.N.N). # Reads from stdin, writes to stdout. version_sort_desc() { - LC_ALL=C sort -t. -k1,1Vr -k2,2nr -k3,3nr + LC_ALL=C sort -t. -k1,1Vr -k2,2nr -k3,3nr } # Find the latest semver tag for a given major. # Usage: latest_semver_for_major v1 → v1.2.3 latest_semver_for_major() { - local major="$1" - git tag -l "${major}.*" | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | version_sort_desc | head -n1 + local major="$1" + git tag -l "${major}.*" | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | version_sort_desc | head -n1 } # Suggest a moving major tag. # Prefers: (a) existing vN tag pointing at HEAD, (b) major from latest semver, (c) v1. suggest_major_tag() { - local head - head="$(git rev-parse HEAD)" - local pointing_major - pointing_major="$(git tag --points-at "$head" | grep -E '^v[0-9]+$' | head -n1 || true)" - if [[ -n "$pointing_major" ]]; then - echo "$pointing_major"; return - fi - local latest_any - latest_any="$(git tag -l 'v*.*.*' | version_sort_desc | head -n1 || true)" - if [[ -n "$latest_any" ]]; then - major_of_semver "$latest_any"; return - fi - echo "v1" + local head + head="$(git rev-parse HEAD)" + local pointing_major + pointing_major="$(git tag --points-at "$head" | grep -E '^v[0-9]+$' | head -n1 || true)" + if [[ -n "$pointing_major" ]]; then + echo "$pointing_major" + return + fi + local latest_any + latest_any="$(git tag -l 'v*.*.*' | version_sort_desc | head -n1 || true)" + if [[ -n "$latest_any" ]]; then + major_of_semver "$latest_any" + return + fi + echo "v1" } # Suggest the next semver tag for a given major. # Defaults to patch bump of latest semver, else starts at vN.0.0. suggest_semver_bump() { - local major="$1" - local latest - latest="$(latest_semver_for_major "$major" || true)" - if [[ -z "$latest" ]]; then - echo "${major}.0.0"; return - fi - if ! [[ "$latest" =~ ^v([0-9]+)\.([0-9]+)\.([0-9]+)$ ]]; then - echo "${major}.0.0"; return - fi - local M="${BASH_REMATCH[1]}" m="${BASH_REMATCH[2]}" p="${BASH_REMATCH[3]}" - printf "v%d.%d.%d" "$M" "$m" "$((p+1))" + local major="$1" + local latest + latest="$(latest_semver_for_major "$major" || true)" + if [[ -z "$latest" ]]; then + echo "${major}.0.0" + return + fi + if ! [[ "$latest" =~ ^v([0-9]+)\.([0-9]+)\.([0-9]+)$ ]]; then + echo "${major}.0.0" + return + fi + local M="${BASH_REMATCH[1]}" m="${BASH_REMATCH[2]}" p="${BASH_REMATCH[3]}" + printf "v%d.%d.%d" "$M" "$m" "$((p + 1))" } # Prompt the user with a yes/no question. # Usage: if confirm "Proceed?"; then ... confirm() { - local prompt="$1" - read -r -p "$prompt [y/N]: " ans - [[ "${ans,,}" == "y" || "${ans,,}" == "yes" ]] + local prompt="$1" + read -r -p "$prompt [y/N]: " ans + [[ "${ans,,}" == "y" || "${ans,,}" == "yes" ]] } # Push the semver tag and (safely) move the moving-major tag together. # Usage: push_tags_safely [extra_push_flags] push_tags_safely() { - local remote="$1" major="$2" semver="$3" extra="${4:-}" - [[ -n "$remote" && -n "$major" && -n "$semver" ]] || { - echo "push_tags_safely: remote/major/semver required" >&2; return 2; } - - echo "Pushing tags atomically to '$remote' ..." - # Current remote OID for the moving major tag (if any) - local old_oid - old_oid="$(git ls-remote --tags "$remote" "$major" | awk '{print $1}' | tail -n1)" - - if [[ -n "$old_oid" ]]; then - # Tag exists remotely → only move it if nobody else moved it since we looked - _run git push "$remote" --atomic $extra \ - --force-with-lease="refs/tags/$major:$old_oid" \ - "refs/tags/$semver:refs/tags/$semver" \ - "refs/tags/$major:refs/tags/$major" - else - # Tag does not exist → assert non-existence with an empty expected value - _run git push "$remote" --atomic $extra \ - --force-with-lease="refs/tags/$major:" \ - "refs/tags/$semver:refs/tags/$semver" \ - "refs/tags/$major:refs/tags/$major" - fi + local remote="$1" major="$2" semver="$3" extra="${4:-}" + [[ -n "$remote" && -n "$major" && -n "$semver" ]] || { + echo "push_tags_safely: remote/major/semver required" >&2 + return 2 + } + + echo "Pushing tags atomically to '$remote' ..." + # Current remote OID for the moving major tag (if any) + local old_oid + old_oid="$(git ls-remote --tags "$remote" "$major" | awk '{print $1}' | tail -n1)" + + if [[ -n "$old_oid" ]]; then + # Tag exists remotely → only move it if nobody else moved it since we looked + _run git push "$remote" --atomic "$extra" \ + --force-with-lease="refs/tags/$major:$old_oid" \ + "refs/tags/$semver:refs/tags/$semver" \ + "refs/tags/$major:refs/tags/$major" + else + # Tag does not exist → assert non-existence with an empty expected value + _run git push "$remote" --atomic "$extra" \ + --force-with-lease="refs/tags/$major:" \ + "refs/tags/$semver:refs/tags/$semver" \ + "refs/tags/$major:refs/tags/$major" + fi } # --- Main ------------------------------------------------------------------ @@ -228,8 +237,8 @@ is_valid_semver_tag "$semver_tag" || die "Invalid semver tag '$semver_tag' (expe # Ensure chosen semver matches chosen major semver_major="$(major_of_semver "$semver_tag")" if [[ "$semver_major" != "$major_tag" ]]; then - echo "Warning: semver major '$semver_major' does not match moving major '$major_tag'." - confirm "Continue anyway?" || exit 1 + echo "Warning: semver major '$semver_major' does not match moving major '$major_tag'." + confirm "Continue anyway?" || exit 1 fi # Message @@ -239,13 +248,13 @@ tag_msg="${tag_msg:-$default_msg}" # Pre-flight checks if git rev-parse -q --verify "refs/tags/$semver_tag" >/dev/null; then - die "Semver tag '$semver_tag' already exists. Choose another." + die "Semver tag '$semver_tag' already exists. Choose another." fi # If the moving major exists, we will move it (force). Ask for confirmation. if git rev-parse -q --verify "refs/tags/$major_tag" >/dev/null; then - echo "Moving major tag '$major_tag' already exists and will be updated to this commit." - confirm "Proceed to move '$major_tag'?" || exit 1 + echo "Moving major tag '$major_tag' already exists and will be updated to this commit." + confirm "Proceed to move '$major_tag'?" || exit 1 fi # Create / move tags to current HEAD @@ -259,9 +268,9 @@ push_tags_safely "$UPSTREAM_REMOTE" "$major_tag" "$semver_tag" "--force-if-inclu echo "Done!" if [[ "${DRY_RUN:-0}" == 1 ]]; then - echo " (dry-run) Moving major: $major_tag -> would point to $head_short" - echo " (dry-run) Semver : $semver_tag -> would point to $head_short" + echo " (dry-run) Moving major: $major_tag -> would point to $head_short" + echo " (dry-run) Semver : $semver_tag -> would point to $head_short" else - echo " Moving major: $major_tag -> $(git rev-parse --short "$major_tag")" - echo " Semver : $semver_tag -> $(git rev-parse --short "$semver_tag")" + echo " Moving major: $major_tag -> $(git rev-parse --short "$major_tag")" + echo " Semver : $semver_tag -> $(git rev-parse --short "$semver_tag")" fi