From 00803237db5d0c8900d6dcfbf456e8712d9cda65 Mon Sep 17 00:00:00 2001 From: Jimisola Laursen Date: Mon, 4 May 2026 10:25:53 +0200 Subject: [PATCH 01/11] feat(ci): add centralized reusable release and publish workflows Implements reqstool/reqstool.github.io#33. Adds 10 reusable workflows organized by language under .github/workflows/: - common/: check-release, release, release-preview, publish-to-docker - java/: publish-to-maven, publish-to-gradle (target: portal|central) - python/: publish-to-python, publish-to-python-test - typescript/: publish-to-npm, publish-to-vscode All publish workflows support a dry-run input for CI-safe validation without uploading to external registries. release-preview.yml computes the next version via git-cliff and optionally cross-checks it against the calling repo's build tool via a version-command input. Adds tests/ mirroring the workflow directory structure (1:1 file mapping) with act-compatible test workflows and JSON event fixtures for failure-path testing of check-release.yml. Adds ci.yml that runs actionlint, zizmor, yamllint, and act-based behavioral tests on any change to .github/workflows/** or tests/**. Adds .github/cliff.toml as the org-wide default changelog config. Signed-off-by: Jimisola Laursen --- .github/cliff.toml | 33 ++++ .github/workflows/ci.yml | 164 ++++++++++++++++++ .github/workflows/common/check-release.yml | 58 +++++++ .../workflows/common/publish-to-docker.yml | 64 +++++++ .github/workflows/common/release-preview.yml | 133 ++++++++++++++ .github/workflows/common/release.yml | 131 ++++++++++++++ .github/workflows/java/publish-to-gradle.yml | 64 +++++++ .github/workflows/java/publish-to-maven.yml | 64 +++++++ .../python/publish-to-python-test.yml | 39 +++++ .../workflows/python/publish-to-python.yml | 42 +++++ .../workflows/typescript/publish-to-npm.yml | 44 +++++ .../typescript/publish-to-vscode.yml | 69 ++++++++ .../_resources/fake-docker-context/Dockerfile | 3 + .../_resources/fake-npm-package/package.json | 10 ++ tests/common/check-release.yml | 24 +++ tests/common/check-release/hotfix-branch.json | 7 + .../common/check-release/invalid-pep440.json | 7 + .../common/check-release/invalid-semver.json | 7 + tests/common/check-release/not-a-tag.json | 7 + .../common/check-release/release-branch.json | 7 + tests/common/check-release/valid-maven.json | 7 + tests/common/check-release/valid-pep440.json | 7 + tests/common/check-release/valid-semver.json | 7 + tests/common/check-release/wrong-branch.json | 7 + tests/common/publish-to-docker.yml | 20 +++ tests/common/release-preview.yml | 15 ++ tests/common/release.yml | 36 ++++ tests/java/publish-to-gradle.yml | 21 +++ tests/java/publish-to-maven.yml | 18 ++ tests/python/publish-to-python-test.yml | 15 ++ tests/python/publish-to-python.yml | 15 ++ tests/typescript/publish-to-npm.yml | 15 ++ tests/typescript/publish-to-vscode.yml | 16 ++ 33 files changed, 1176 insertions(+) create mode 100644 .github/cliff.toml create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/common/check-release.yml create mode 100644 .github/workflows/common/publish-to-docker.yml create mode 100644 .github/workflows/common/release-preview.yml create mode 100644 .github/workflows/common/release.yml create mode 100644 .github/workflows/java/publish-to-gradle.yml create mode 100644 .github/workflows/java/publish-to-maven.yml create mode 100644 .github/workflows/python/publish-to-python-test.yml create mode 100644 .github/workflows/python/publish-to-python.yml create mode 100644 .github/workflows/typescript/publish-to-npm.yml create mode 100644 .github/workflows/typescript/publish-to-vscode.yml create mode 100644 tests/_resources/fake-docker-context/Dockerfile create mode 100644 tests/_resources/fake-npm-package/package.json create mode 100644 tests/common/check-release.yml create mode 100644 tests/common/check-release/hotfix-branch.json create mode 100644 tests/common/check-release/invalid-pep440.json create mode 100644 tests/common/check-release/invalid-semver.json create mode 100644 tests/common/check-release/not-a-tag.json create mode 100644 tests/common/check-release/release-branch.json create mode 100644 tests/common/check-release/valid-maven.json create mode 100644 tests/common/check-release/valid-pep440.json create mode 100644 tests/common/check-release/valid-semver.json create mode 100644 tests/common/check-release/wrong-branch.json create mode 100644 tests/common/publish-to-docker.yml create mode 100644 tests/common/release-preview.yml create mode 100644 tests/common/release.yml create mode 100644 tests/java/publish-to-gradle.yml create mode 100644 tests/java/publish-to-maven.yml create mode 100644 tests/python/publish-to-python-test.yml create mode 100644 tests/python/publish-to-python.yml create mode 100644 tests/typescript/publish-to-npm.yml create mode 100644 tests/typescript/publish-to-vscode.yml diff --git a/.github/cliff.toml b/.github/cliff.toml new file mode 100644 index 0000000..1764f1f --- /dev/null +++ b/.github/cliff.toml @@ -0,0 +1,33 @@ +[changelog] +header = "" +body = """ +{% for group, commits in commits | group_by(attribute="group") %}\ +### {{ group | upper_first }} +{% for commit in commits %}\ +- {{ commit.message | upper_first }}{% if commit.remote.pr_number %} (#{{ commit.remote.pr_number }}){% endif %} + +{% endfor %}\ +{% endfor %}\ +""" +trim = true +footer = "" + +[git] +conventional_commits = true +filter_unconventional = true +commit_parsers = [ + { message = "^feat", group = "Features" }, + { message = "^fix", group = "Bug Fixes" }, + { message = "^perf", group = "Performance" }, + { message = "^refactor", group = "Refactoring" }, + { message = "^chore", group = "Miscellaneous" }, + { message = "^docs", group = "Documentation" }, + { message = "^test", group = "Testing" }, + { message = "^security", group = "Security" }, + { message = "^style", group = "Style" }, + { message = "^ci", group = "CI/CD", skip = true }, + { message = "^build", group = "Build", skip = true }, +] +filter_commits = false +tag_pattern = "[0-9].*" +sort_commits = "newest" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..db96bd6 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,164 @@ +name: CI + +on: + push: + paths: + - ".github/workflows/**" + - "tests/**" + pull_request: + paths: + - ".github/workflows/**" + - "tests/**" + +jobs: + lint: + name: Lint workflows + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install actionlint + run: | + VERSION="1.7.7" + curl -fsSL \ + "https://github.com/rhysd/actionlint/releases/download/v${VERSION}/actionlint_${VERSION}_linux_amd64.tar.gz" \ + | tar -xz actionlint + sudo mv actionlint /usr/local/bin/ + + - name: Install zizmor + run: pip install zizmor + + - name: Install yamllint + run: pip install yamllint + + - name: actionlint + run: actionlint .github/workflows/**/*.yml .github/workflows/*.yml + + - name: zizmor + run: zizmor --format sarif .github/workflows/ > zizmor.sarif || true + + - name: yamllint + run: | + yamllint -d "{extends: relaxed, rules: {line-length: {max: 200}}}" \ + .github/workflows/ + + test-check-release: + name: Test — common/check-release + needs: lint + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + fixture: + - valid-semver + - invalid-semver + - valid-pep440 + - invalid-pep440 + - valid-maven + - not-a-tag + - wrong-branch + - hotfix-branch + - release-branch + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Install act + run: | + VERSION="0.2.74" + curl -fsSL \ + "https://github.com/nektos/act/releases/download/v${VERSION}/act_Linux_x86_64.tar.gz" \ + | tar -xz act + sudo mv act /usr/local/bin/ + + - name: Read fixture + id: fixture + run: | + FIXTURE="tests/common/check-release/${{ matrix.fixture }}.json" + EXPECTED=$(jq -r '.expected_exit' "$FIXTURE") + GITHUB_REF=$(jq -r '.github_ref' "$FIXTURE") + VERSION_FORMAT=$(jq -r '.inputs["version-format"]' "$FIXTURE") + TARGET_COMMITISH=$(jq -r '.event.release.target_commitish' "$FIXTURE") + echo "expected=$EXPECTED" >> "$GITHUB_OUTPUT" + echo "github_ref=$GITHUB_REF" >> "$GITHUB_OUTPUT" + echo "version_format=$VERSION_FORMAT" >> "$GITHUB_OUTPUT" + echo "target_commitish=$TARGET_COMMITISH" >> "$GITHUB_OUTPUT" + + - name: Build event payload + run: | + jq -n \ + --arg commitish "${{ steps.fixture.outputs.target_commitish }}" \ + '{"release": {"target_commitish": $commitish}}' \ + > /tmp/event.json + + - name: Run act + id: act + continue-on-error: true + run: | + act workflow_call \ + -W .github/workflows/common/check-release.yml \ + --eventpath /tmp/event.json \ + --env "GITHUB_REF=${{ steps.fixture.outputs.github_ref }}" \ + --env "GITHUB_REF_NAME=$(echo '${{ steps.fixture.outputs.github_ref }}' | sed 's|refs/tags/||;s|refs/heads/||')" \ + --input "version-format=${{ steps.fixture.outputs.version_format }}" \ + --no-cache-server \ + -q + + - name: Assert outcome + run: | + ACT_EXIT=${{ steps.act.outcome == 'success' && '0' || '1' }} + EXPECTED="${{ steps.fixture.outputs.expected }}" + if [ "$ACT_EXIT" != "$EXPECTED" ]; then + echo "::error::Fixture '${{ matrix.fixture }}': expected exit $EXPECTED but got $ACT_EXIT" + exit 1 + fi + echo "Fixture '${{ matrix.fixture }}': exit $ACT_EXIT — PASS" + + test-release-dry-run: + name: Test — common/release (dry-run) + needs: lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Install act + run: | + VERSION="0.2.74" + curl -fsSL \ + "https://github.com/nektos/act/releases/download/v${VERSION}/act_Linux_x86_64.tar.gz" \ + | tar -xz act + sudo mv act /usr/local/bin/ + + - name: Run release dry-run (semver) + run: | + act workflow_call \ + -W tests/common/release.yml \ + --no-cache-server \ + -q + + test-release-preview: + name: Test — common/release-preview + needs: lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Install act + run: | + VERSION="0.2.74" + curl -fsSL \ + "https://github.com/nektos/act/releases/download/v${VERSION}/act_Linux_x86_64.tar.gz" \ + | tar -xz act + sudo mv act /usr/local/bin/ + + - name: Run release-preview + run: | + act workflow_call \ + -W tests/common/release-preview.yml \ + --no-cache-server \ + -q diff --git a/.github/workflows/common/check-release.yml b/.github/workflows/common/check-release.yml new file mode 100644 index 0000000..1522036 --- /dev/null +++ b/.github/workflows/common/check-release.yml @@ -0,0 +1,58 @@ +name: Check Release + +on: + workflow_call: + inputs: + version-format: + description: "Version format to validate: semver | pep440 | maven" + required: true + type: string + +jobs: + check-release: + runs-on: ubuntu-latest + steps: + - name: Check ref is a tag + run: | + if [[ "${{ github.ref }}" != refs/tags/* ]]; then + echo "::error::Expected a tag ref (refs/tags/...) but got: ${{ github.ref }}" + exit 1 + fi + + - name: Validate tag format (${{ inputs.version-format }}) + run: | + TAG="${{ github.ref_name }}" + FORMAT="${{ inputs.version-format }}" + + case "$FORMAT" in + semver) + if ! npx --yes semver "$TAG" > /dev/null 2>&1; then + echo "::error::Tag '$TAG' is not valid npm semver (expected X.Y.Z or X.Y.Z-pre.N, no v prefix)" + exit 1 + fi + ;; + pep440) + if ! echo "$TAG" | grep -qE '^[0-9]+(\.[0-9]+)*(a[0-9]+|b[0-9]+|rc[0-9]+)?(\.post[0-9]+)?(\.dev[0-9]+)?$'; then + echo "::error::Tag '$TAG' is not a valid PEP 440 version (e.g. 1.2.3, 1.2.3rc1, 1.2.3.post1 — no v prefix)" + exit 1 + fi + ;; + maven) + if ! echo "$TAG" | grep -qE '^[0-9]+(\.[0-9]+)*(-[A-Za-z0-9._-]+)?$'; then + echo "::error::Tag '$TAG' is not a valid Maven version (e.g. 1.2.3, 1.2.3-RELEASE — no v prefix)" + exit 1 + fi + ;; + *) + echo "::error::Unknown version-format '$FORMAT'. Must be semver, pep440, or maven." + exit 1 + ;; + esac + + - name: Check release branch + run: | + BRANCH="${{ github.event.release.target_commitish }}" + if [[ "$BRANCH" != "main" && "$BRANCH" != hotfix/* && "$BRANCH" != release/* ]]; then + echo "::error::Release must target main, hotfix/*, or release/* (got: '$BRANCH')" + exit 1 + fi diff --git a/.github/workflows/common/publish-to-docker.yml b/.github/workflows/common/publish-to-docker.yml new file mode 100644 index 0000000..9a02fbc --- /dev/null +++ b/.github/workflows/common/publish-to-docker.yml @@ -0,0 +1,64 @@ +name: Publish to Docker Registry + +on: + workflow_call: + inputs: + version: + description: "Semver version to tag (no v prefix, e.g. 1.2.3)" + required: true + type: string + image-name: + description: "Image name without registry prefix (e.g. reqstool-org/reqstool)" + required: true + type: string + registry: + description: "Container registry hostname" + required: false + type: string + default: "ghcr.io" + dry-run: + description: "Build the image but do NOT push. No registry login required." + required: false + type: boolean + default: false + +jobs: + publish: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + id-token: write + steps: + - uses: actions/checkout@v4 + + - name: Docker metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ inputs.registry }}/${{ inputs.image-name }} + tags: | + type=semver,pattern={{version}},value=${{ inputs.version }} + type=semver,pattern={{major}}.{{minor}},value=${{ inputs.version }} + type=semver,pattern={{major}},value=${{ inputs.version }},enable=${{ !startsWith(inputs.version, '0.') }} + type=sha + type=raw,value=latest + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to registry + if: ${{ !inputs.dry-run }} + uses: docker/login-action@v3 + with: + registry: ${{ inputs.registry }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build and push + uses: docker/build-push-action@v6 + with: + push: ${{ !inputs.dry-run }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + build-args: VERSION=${{ inputs.version }} diff --git a/.github/workflows/common/release-preview.yml b/.github/workflows/common/release-preview.yml new file mode 100644 index 0000000..b10d622 --- /dev/null +++ b/.github/workflows/common/release-preview.yml @@ -0,0 +1,133 @@ +name: Release Preview + +on: + workflow_call: + inputs: + cliff-config: + description: "Path to cliff.toml in calling repo. Empty = use org default." + required: false + type: string + default: "" + version-command: + description: > + Shell command that prints the current build-tool version to stdout. + Used to cross-check against git-cliff's computed next version. + Examples: + Python/hatch: hatch version + Python/poetry: poetry version --short + Maven: mvn help:evaluate -Dexpression=project.version -q -DforceStdout + npm: node -p "require('./package.json').version" + Gradle: ./gradlew properties -q | grep '^version:' | awk '{print $2}' + required: false + type: string + default: "" + +jobs: + preview: + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + fetch-tags: true + + - name: Set up cliff.toml + run: | + CONFIG="${{ inputs.cliff-config }}" + if [ -n "$CONFIG" ] && [ -f "$CONFIG" ]; then + cp "$CONFIG" /tmp/cliff.toml + elif [ -f "cliff.toml" ]; then + cp cliff.toml /tmp/cliff.toml + else + curl -fsSL \ + "https://raw.githubusercontent.com/reqstool/.github/main/.github/cliff.toml" \ + -o /tmp/cliff.toml + fi + + - name: Get latest tag + id: latest + run: | + TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "(no tags yet)") + echo "tag=$TAG" >> "$GITHUB_OUTPUT" + + - name: Compute next version via git-cliff + id: next + uses: orhun/git-cliff-action@v4 + with: + config: /tmp/cliff.toml + args: --bumped-version + env: + OUTPUT: /dev/null + + - name: Generate draft release notes + uses: orhun/git-cliff-action@v4 + with: + config: /tmp/cliff.toml + args: --latest --unreleased --strip all + env: + OUTPUT: NOTES.md + + - name: List commits since latest tag + id: commits + run: | + LATEST="${{ steps.latest.outputs.tag }}" + if [ "$LATEST" = "(no tags yet)" ]; then + COMMITS=$(git log --format="- %h %s" | head -20) + else + COMMITS=$(git log "${LATEST}..HEAD" --format="- %h %s") + fi + echo "COMMITS<> "$GITHUB_ENV" + echo "$COMMITS" >> "$GITHUB_ENV" + echo "EOF" >> "$GITHUB_ENV" + + - name: Get build-tool version + id: build-version + if: ${{ inputs.version-command != '' }} + run: | + BUILD_VERSION=$(eval "${{ inputs.version-command }}" 2>/dev/null || echo "(error)") + echo "version=$BUILD_VERSION" >> "$GITHUB_OUTPUT" + + - name: Compare versions + id: compare + if: ${{ inputs.version-command != '' }} + run: | + NEXT="${{ steps.next.outputs.version }}" + BUILD="${{ steps.build-version.outputs.version }}" + BASE=$(echo "$BUILD" | grep -oE '^[0-9]+\.[0-9]+\.[0-9]+' || echo "") + if [ -n "$BASE" ] && [ "$BASE" != "$NEXT" ]; then + echo "mismatch=true" >> "$GITHUB_OUTPUT" + echo "message=⚠️ Mismatch: git-cliff suggests \`$NEXT\` but build tool reports \`$BUILD\`" >> "$GITHUB_OUTPUT" + else + echo "mismatch=false" >> "$GITHUB_OUTPUT" + echo "message=✅ \`$BUILD\`" >> "$GITHUB_OUTPUT" + fi + + - name: Write job summary + run: | + LATEST="${{ steps.latest.outputs.tag }}" + NEXT="${{ steps.next.outputs.version }}" + REPO="${{ github.server_url }}/${{ github.repository }}" + + { + echo "## Release Preview" + echo "" + echo "| | Value |" + echo "|---|---|" + echo "| Latest tag | \`${LATEST}\` |" + echo "| Next version (git-cliff) | \`${NEXT}\` |" + if [ -n "${{ steps.compare.outputs.message }}" ]; then + echo "| Build tool version | ${{ steps.compare.outputs.message }} |" + fi + echo "" + echo "### Commits since \`${LATEST}\`" + echo "" + echo "$COMMITS" + echo "" + echo "### Draft release notes" + echo "" + cat NOTES.md + echo "" + echo "[→ GitHub Releases page](${REPO}/releases)" + } >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/common/release.yml b/.github/workflows/common/release.yml new file mode 100644 index 0000000..e4e18e7 --- /dev/null +++ b/.github/workflows/common/release.yml @@ -0,0 +1,131 @@ +name: Release + +on: + workflow_call: + inputs: + version: + description: "Version string to release (no v prefix)" + required: true + type: string + version-format: + description: "Version format: semver | pep440 | maven" + required: true + type: string + cliff-config: + description: "Path to cliff.toml in the calling repo. Empty = use org default." + required: false + type: string + default: "" + dry-run: + description: "Validate and preview only — no tag push, no draft release created." + required: false + type: boolean + default: false + +jobs: + release: + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + fetch-tags: true + + - name: Validate version format (${{ inputs.version-format }}) + run: | + VERSION="${{ inputs.version }}" + FORMAT="${{ inputs.version-format }}" + + case "$FORMAT" in + semver) + if ! npx --yes semver "$VERSION" > /dev/null 2>&1; then + echo "::error::Version '$VERSION' is not valid npm semver (expected X.Y.Z or X.Y.Z-pre.N)" + exit 1 + fi + ;; + pep440) + if ! echo "$VERSION" | grep -qE '^[0-9]+(\.[0-9]+)*(a[0-9]+|b[0-9]+|rc[0-9]+)?(\.post[0-9]+)?(\.dev[0-9]+)?$'; then + echo "::error::Version '$VERSION' is not a valid PEP 440 version (e.g. 1.2.3, 1.2.3rc1)" + exit 1 + fi + ;; + maven) + if ! echo "$VERSION" | grep -qE '^[0-9]+(\.[0-9]+)*(-[A-Za-z0-9._-]+)?$'; then + echo "::error::Version '$VERSION' is not a valid Maven version (e.g. 1.2.3, 1.2.3-RELEASE)" + exit 1 + fi + ;; + *) + echo "::error::Unknown version-format '$FORMAT'." + exit 1 + ;; + esac + + - name: Check tag does not already exist + run: | + if git rev-parse "${{ inputs.version }}" > /dev/null 2>&1; then + echo "::error::Tag '${{ inputs.version }}' already exists" + exit 1 + fi + + - name: Set up cliff.toml + run: | + CONFIG="${{ inputs.cliff-config }}" + if [ -n "$CONFIG" ] && [ -f "$CONFIG" ]; then + cp "$CONFIG" /tmp/cliff.toml + elif [ -f "cliff.toml" ]; then + cp cliff.toml /tmp/cliff.toml + else + curl -fsSL \ + "https://raw.githubusercontent.com/reqstool/.github/main/.github/cliff.toml" \ + -o /tmp/cliff.toml + fi + + - name: Generate changelog + uses: orhun/git-cliff-action@v4 + with: + config: /tmp/cliff.toml + args: --latest --unreleased --strip all + env: + OUTPUT: NOTES.md + + - name: Push tag + if: ${{ !inputs.dry-run }} + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git tag "${{ inputs.version }}" + git push origin "${{ inputs.version }}" + + - name: Create draft GitHub Release + if: ${{ !inputs.dry-run }} + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + DRAFT_URL=$(gh release create "${{ inputs.version }}" \ + --title "${{ inputs.version }}" \ + --notes-file NOTES.md \ + --draft \ + --json url -q .url) + echo "### Release draft created" >> "$GITHUB_STEP_SUMMARY" + echo "" >> "$GITHUB_STEP_SUMMARY" + echo "**Version:** \`${{ inputs.version }}\`" >> "$GITHUB_STEP_SUMMARY" + echo "" >> "$GITHUB_STEP_SUMMARY" + echo "**Draft URL:** $DRAFT_URL" >> "$GITHUB_STEP_SUMMARY" + echo "" >> "$GITHUB_STEP_SUMMARY" + echo "Review the draft and click **Publish release** to trigger the publish workflows." >> "$GITHUB_STEP_SUMMARY" + + - name: Dry-run summary + if: ${{ inputs.dry-run }} + run: | + echo "### Release dry-run" >> "$GITHUB_STEP_SUMMARY" + echo "" >> "$GITHUB_STEP_SUMMARY" + echo "**Version:** \`${{ inputs.version }}\` " >> "$GITHUB_STEP_SUMMARY" + echo "**Format:** \`${{ inputs.version-format }}\` " >> "$GITHUB_STEP_SUMMARY" + echo "**Mode:** dry-run — no tag pushed, no release created." >> "$GITHUB_STEP_SUMMARY" + echo "" >> "$GITHUB_STEP_SUMMARY" + echo "#### Generated release notes" >> "$GITHUB_STEP_SUMMARY" + echo "" >> "$GITHUB_STEP_SUMMARY" + cat NOTES.md >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/java/publish-to-gradle.yml b/.github/workflows/java/publish-to-gradle.yml new file mode 100644 index 0000000..76d8942 --- /dev/null +++ b/.github/workflows/java/publish-to-gradle.yml @@ -0,0 +1,64 @@ +name: Publish via Gradle + +on: + workflow_call: + inputs: + target: + description: "Publish target: portal (Gradle Plugin Portal) or central (Maven Central / Sonatype)." + required: true + type: string + java-version: + description: "Java version to use." + required: false + type: string + default: "21" + dry-run: + description: "Build without publishing (./gradlew build -x test). No credentials needed." + required: false + type: boolean + default: false + secrets: + GRADLE_PUBLISH_KEY: + required: false + GRADLE_PUBLISH_SECRET: + required: false + REQSTOOL_PRIVATE_GPG_KEY: + required: false + REQSTOOL_PRIVATE_GPG_PASSPHRASE: + required: false + OSSRH_USERNAME: + required: false + OSSRH_TOKEN: + required: false + +jobs: + publish: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Java + uses: actions/setup-java@v4 + with: + java-version: ${{ inputs.java-version }} + distribution: temurin + + - name: Dry-run build + if: ${{ inputs.dry-run }} + run: ./gradlew build -x test + + - name: Publish to Gradle Plugin Portal + if: ${{ !inputs.dry-run && inputs.target == 'portal' }} + run: ./gradlew publishPlugins + env: + GRADLE_PUBLISH_KEY: ${{ secrets.GRADLE_PUBLISH_KEY }} + GRADLE_PUBLISH_SECRET: ${{ secrets.GRADLE_PUBLISH_SECRET }} + + - name: Publish to Maven Central (Sonatype) + if: ${{ !inputs.dry-run && inputs.target == 'central' }} + run: ./gradlew publish + env: + REQSTOOL_PRIVATE_GPG_KEY: ${{ secrets.REQSTOOL_PRIVATE_GPG_KEY }} + REQSTOOL_PRIVATE_GPG_PASSPHRASE: ${{ secrets.REQSTOOL_PRIVATE_GPG_PASSPHRASE }} + OSSRH_USERNAME: ${{ secrets.OSSRH_USERNAME }} + OSSRH_TOKEN: ${{ secrets.OSSRH_TOKEN }} diff --git a/.github/workflows/java/publish-to-maven.yml b/.github/workflows/java/publish-to-maven.yml new file mode 100644 index 0000000..fdc2fbd --- /dev/null +++ b/.github/workflows/java/publish-to-maven.yml @@ -0,0 +1,64 @@ +name: Publish to Maven Central + +on: + workflow_call: + inputs: + java-version: + description: "Java version to use." + required: false + type: string + default: "21" + dry-run: + description: "Build without deploying (mvn package -DskipTests). No GPG or token needed." + required: false + type: boolean + default: false + secrets: + MAVEN_CENTRAL_USERNAME: + required: false + MAVEN_CENTRAL_TOKEN: + required: false + REQSTOOL_PRIVATE_GPG_KEY: + required: false + REQSTOOL_PRIVATE_GPG_PASSPHRASE: + required: false + +jobs: + publish: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + steps: + - uses: actions/checkout@v4 + + - name: Set up Java (dry-run) + if: ${{ inputs.dry-run }} + uses: actions/setup-java@v4 + with: + java-version: ${{ inputs.java-version }} + distribution: temurin + + - name: Set up Java (publish) + if: ${{ !inputs.dry-run }} + uses: actions/setup-java@v4 + with: + java-version: ${{ inputs.java-version }} + distribution: temurin + server-id: central + server-username: MAVEN_CENTRAL_USERNAME + server-password: MAVEN_CENTRAL_TOKEN + gpg-private-key: ${{ secrets.REQSTOOL_PRIVATE_GPG_KEY }} + gpg-passphrase: REQSTOOL_PRIVATE_GPG_PASSPHRASE + + - name: Dry-run build + if: ${{ inputs.dry-run }} + run: mvn clean package -DskipTests + + - name: Publish to Maven Central + if: ${{ !inputs.dry-run }} + run: mvn clean deploy + env: + MAVEN_CENTRAL_USERNAME: ${{ secrets.MAVEN_CENTRAL_USERNAME }} + MAVEN_CENTRAL_TOKEN: ${{ secrets.MAVEN_CENTRAL_TOKEN }} + REQSTOOL_PRIVATE_GPG_PASSPHRASE: ${{ secrets.REQSTOOL_PRIVATE_GPG_PASSPHRASE }} diff --git a/.github/workflows/python/publish-to-python-test.yml b/.github/workflows/python/publish-to-python-test.yml new file mode 100644 index 0000000..ee497d5 --- /dev/null +++ b/.github/workflows/python/publish-to-python-test.yml @@ -0,0 +1,39 @@ +name: Publish to Test PyPI + +on: + workflow_call: + inputs: + dry-run: + description: "Run twine check instead of uploading to Test PyPI." + required: false + type: boolean + default: false + +jobs: + publish: + runs-on: ubuntu-latest + environment: + name: test + url: https://test.pypi.org + permissions: + id-token: write + steps: + - name: Download dist artifacts + uses: actions/download-artifact@v4 + with: + name: dist + path: dist + + - name: Dry-run — validate artifacts + if: ${{ inputs.dry-run }} + run: | + pip install --quiet twine + twine check dist/* + + - name: Publish to Test PyPI + if: ${{ !inputs.dry-run }} + uses: pypa/gh-action-pypi-publish@release/v1 + with: + repository-url: https://test.pypi.org/legacy/ + attestations: true + skip-existing: true diff --git a/.github/workflows/python/publish-to-python.yml b/.github/workflows/python/publish-to-python.yml new file mode 100644 index 0000000..dd58370 --- /dev/null +++ b/.github/workflows/python/publish-to-python.yml @@ -0,0 +1,42 @@ +name: Publish to PyPI + +on: + workflow_call: + inputs: + dry-run: + description: "Run twine check instead of uploading to PyPI." + required: false + type: boolean + default: false + environment: + description: "GitHub environment to deploy to." + required: false + type: string + default: "prod" + +jobs: + publish: + runs-on: ubuntu-latest + environment: + name: ${{ inputs.environment }} + url: https://pypi.org + permissions: + id-token: write + steps: + - name: Download dist artifacts + uses: actions/download-artifact@v4 + with: + name: dist + path: dist + + - name: Dry-run — validate artifacts + if: ${{ inputs.dry-run }} + run: | + pip install --quiet twine + twine check dist/* + + - name: Publish to PyPI + if: ${{ !inputs.dry-run }} + uses: pypa/gh-action-pypi-publish@release/v1 + with: + attestations: true diff --git a/.github/workflows/typescript/publish-to-npm.yml b/.github/workflows/typescript/publish-to-npm.yml new file mode 100644 index 0000000..09359e4 --- /dev/null +++ b/.github/workflows/typescript/publish-to-npm.yml @@ -0,0 +1,44 @@ +name: Publish to npm + +on: + workflow_call: + inputs: + dry-run: + description: "Run npm publish --dry-run. No NPM_TOKEN needed." + required: false + type: boolean + default: false + scope: + description: "npm scope (with @, e.g. @reqstool)." + required: false + type: string + default: "@reqstool" + access: + description: "npm publish access level." + required: false + type: string + default: "public" + secrets: + NPM_TOKEN: + required: false + +jobs: + publish: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 24 + registry-url: https://registry.npmjs.org + scope: ${{ inputs.scope }} + + - run: npm ci + + - run: npm run build + + - name: Publish + run: npm publish --access ${{ inputs.access }}${{ inputs.dry-run && ' --dry-run' || '' }} + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.github/workflows/typescript/publish-to-vscode.yml b/.github/workflows/typescript/publish-to-vscode.yml new file mode 100644 index 0000000..0924ece --- /dev/null +++ b/.github/workflows/typescript/publish-to-vscode.yml @@ -0,0 +1,69 @@ +name: Publish VS Code Extension + +on: + workflow_call: + inputs: + dry-run: + description: "Validate packaging without uploading to the registry." + required: false + type: boolean + default: false + dependency-version: + description: "Version of a bundled external tool to pin (e.g. a CLI version). Empty = use package.json default." + required: false + type: string + default: "" + dependency-name: + description: "The npm package.json key that holds the bundled tool's version (e.g. reqstoolVersion)." + required: false + type: string + default: "" + secrets: + OPEN_VSX_TOKEN: + required: false + +jobs: + publish: + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - uses: actions/checkout@v4 + with: + fetch-tags: true + + - uses: actions/setup-node@v4 + with: + node-version: 24 + + - run: npm ci + + - name: Pin dependency version + if: ${{ inputs.dependency-version != '' && inputs.dependency-name != '' }} + run: npm pkg set "${{ inputs.dependency-name }}=${{ inputs.dependency-version }}" + + - name: Log effective dependency version + if: ${{ inputs.dependency-name != '' }} + run: | + VERSION=$(node -p "require('./package.json')['${{ inputs.dependency-name }}'] || '(not set)'") + echo "${{ inputs.dependency-name }}: $VERSION" + + - name: Build VSIX + run: npm run build + + - name: Get VSIX path + id: vsix + run: echo "path=$(ls *.vsix | head -1)" >> "$GITHUB_OUTPUT" + + - name: Publish to Open VSX Registry + uses: HaaLeo/publish-vscode-extension@v2 + with: + pat: ${{ secrets.OPEN_VSX_TOKEN }} + extensionFile: ${{ steps.vsix.outputs.path }} + dryRun: ${{ inputs.dry-run }} + + - name: Upload VSIX to GitHub Release + if: ${{ !inputs.dry-run && github.event_name == 'release' }} + run: gh release upload "${{ github.ref_name }}" "${{ steps.vsix.outputs.path }}" + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/tests/_resources/fake-docker-context/Dockerfile b/tests/_resources/fake-docker-context/Dockerfile new file mode 100644 index 0000000..d66e789 --- /dev/null +++ b/tests/_resources/fake-docker-context/Dockerfile @@ -0,0 +1,3 @@ +FROM scratch +ARG VERSION=dev +LABEL org.opencontainers.image.version="${VERSION}" diff --git a/tests/_resources/fake-npm-package/package.json b/tests/_resources/fake-npm-package/package.json new file mode 100644 index 0000000..5c16dad --- /dev/null +++ b/tests/_resources/fake-npm-package/package.json @@ -0,0 +1,10 @@ +{ + "name": "@reqstool/test-package", + "version": "0.0.1", + "description": "Minimal package for workflow dry-run tests", + "main": "dist/index.js", + "scripts": { + "build": "mkdir -p dist && echo 'exports={};' > dist/index.js" + }, + "license": "Apache-2.0" +} diff --git a/tests/common/check-release.yml b/tests/common/check-release.yml new file mode 100644 index 0000000..a30347e --- /dev/null +++ b/tests/common/check-release.yml @@ -0,0 +1,24 @@ +name: Test — common/check-release + +on: + workflow_call: + +# Happy-path tests for .github/workflows/common/check-release.yml. +# Failure-path tests (invalid tag, wrong branch, not-a-tag) are exercised by +# ci.yml via act with event fixtures in tests/common/check-release/*.json. + +jobs: + valid-semver: + uses: ./.github/workflows/common/check-release.yml + with: + version-format: semver + + valid-pep440: + uses: ./.github/workflows/common/check-release.yml + with: + version-format: pep440 + + valid-maven: + uses: ./.github/workflows/common/check-release.yml + with: + version-format: maven diff --git a/tests/common/check-release/hotfix-branch.json b/tests/common/check-release/hotfix-branch.json new file mode 100644 index 0000000..00fc2df --- /dev/null +++ b/tests/common/check-release/hotfix-branch.json @@ -0,0 +1,7 @@ +{ + "_comment": "Valid tag targeting a hotfix/* branch — expect exit 0", + "expected_exit": 0, + "github_ref": "refs/tags/1.2.3", + "inputs": { "version-format": "semver" }, + "event": { "release": { "target_commitish": "hotfix/fix-critical-bug" } } +} diff --git a/tests/common/check-release/invalid-pep440.json b/tests/common/check-release/invalid-pep440.json new file mode 100644 index 0000000..3207ea8 --- /dev/null +++ b/tests/common/check-release/invalid-pep440.json @@ -0,0 +1,7 @@ +{ + "_comment": "PEP 440 tag with v prefix (forbidden) — expect exit 1", + "expected_exit": 1, + "github_ref": "refs/tags/v1.2.3", + "inputs": { "version-format": "pep440" }, + "event": { "release": { "target_commitish": "main" } } +} diff --git a/tests/common/check-release/invalid-semver.json b/tests/common/check-release/invalid-semver.json new file mode 100644 index 0000000..e5b8fb9 --- /dev/null +++ b/tests/common/check-release/invalid-semver.json @@ -0,0 +1,7 @@ +{ + "_comment": "Invalid semver tag — expect exit 1", + "expected_exit": 1, + "github_ref": "refs/tags/bad-ver", + "inputs": { "version-format": "semver" }, + "event": { "release": { "target_commitish": "main" } } +} diff --git a/tests/common/check-release/not-a-tag.json b/tests/common/check-release/not-a-tag.json new file mode 100644 index 0000000..24cb7ea --- /dev/null +++ b/tests/common/check-release/not-a-tag.json @@ -0,0 +1,7 @@ +{ + "_comment": "Branch ref, not a tag — expect exit 1", + "expected_exit": 1, + "github_ref": "refs/heads/main", + "inputs": { "version-format": "semver" }, + "event": { "release": { "target_commitish": "main" } } +} diff --git a/tests/common/check-release/release-branch.json b/tests/common/check-release/release-branch.json new file mode 100644 index 0000000..a222e40 --- /dev/null +++ b/tests/common/check-release/release-branch.json @@ -0,0 +1,7 @@ +{ + "_comment": "Valid tag targeting a release/* branch — expect exit 0", + "expected_exit": 0, + "github_ref": "refs/tags/1.2.3", + "inputs": { "version-format": "semver" }, + "event": { "release": { "target_commitish": "release/1.2" } } +} diff --git a/tests/common/check-release/valid-maven.json b/tests/common/check-release/valid-maven.json new file mode 100644 index 0000000..99f4978 --- /dev/null +++ b/tests/common/check-release/valid-maven.json @@ -0,0 +1,7 @@ +{ + "_comment": "Valid Maven version tag on main — expect exit 0", + "expected_exit": 0, + "github_ref": "refs/tags/1.2.3", + "inputs": { "version-format": "maven" }, + "event": { "release": { "target_commitish": "main" } } +} diff --git a/tests/common/check-release/valid-pep440.json b/tests/common/check-release/valid-pep440.json new file mode 100644 index 0000000..a60eef6 --- /dev/null +++ b/tests/common/check-release/valid-pep440.json @@ -0,0 +1,7 @@ +{ + "_comment": "Valid PEP 440 pre-release tag on main — expect exit 0", + "expected_exit": 0, + "github_ref": "refs/tags/1.2.3rc1", + "inputs": { "version-format": "pep440" }, + "event": { "release": { "target_commitish": "main" } } +} diff --git a/tests/common/check-release/valid-semver.json b/tests/common/check-release/valid-semver.json new file mode 100644 index 0000000..f0e3d39 --- /dev/null +++ b/tests/common/check-release/valid-semver.json @@ -0,0 +1,7 @@ +{ + "_comment": "Valid semver tag on main branch — expect exit 0", + "expected_exit": 0, + "github_ref": "refs/tags/1.2.3", + "inputs": { "version-format": "semver" }, + "event": { "release": { "target_commitish": "main" } } +} diff --git a/tests/common/check-release/wrong-branch.json b/tests/common/check-release/wrong-branch.json new file mode 100644 index 0000000..730572c --- /dev/null +++ b/tests/common/check-release/wrong-branch.json @@ -0,0 +1,7 @@ +{ + "_comment": "Valid tag but release targets a disallowed branch — expect exit 1", + "expected_exit": 1, + "github_ref": "refs/tags/1.2.3", + "inputs": { "version-format": "semver" }, + "event": { "release": { "target_commitish": "develop" } } +} diff --git a/tests/common/publish-to-docker.yml b/tests/common/publish-to-docker.yml new file mode 100644 index 0000000..73ab1bf --- /dev/null +++ b/tests/common/publish-to-docker.yml @@ -0,0 +1,20 @@ +name: Test — common/publish-to-docker + +on: + workflow_call: + +# Tests for .github/workflows/common/publish-to-docker.yml +# Uses dry-run mode: builds the image but does not push. +# Requires tests/_resources/fake-docker-context/Dockerfile. + +jobs: + dry-run: + uses: ./.github/workflows/common/publish-to-docker.yml + with: + version: "1.0.0" + image-name: "reqstool-org/test-image" + dry-run: true + permissions: + contents: read + packages: write + id-token: write diff --git a/tests/common/release-preview.yml b/tests/common/release-preview.yml new file mode 100644 index 0000000..79be835 --- /dev/null +++ b/tests/common/release-preview.yml @@ -0,0 +1,15 @@ +name: Test — common/release-preview + +on: + workflow_call: + +# Tests for .github/workflows/common/release-preview.yml +# The workflow is read-only, so we just confirm it completes successfully. + +jobs: + preview: + uses: ./.github/workflows/common/release-preview.yml + with: + version-command: "" + permissions: + contents: read diff --git a/tests/common/release.yml b/tests/common/release.yml new file mode 100644 index 0000000..0ca87fd --- /dev/null +++ b/tests/common/release.yml @@ -0,0 +1,36 @@ +name: Test — common/release + +on: + workflow_call: + +# Dry-run tests for .github/workflows/common/release.yml. +# No tag is pushed and no draft release is created. +# Failure-path tests (invalid version format) are exercised by ci.yml via act. + +jobs: + dry-run-semver: + uses: ./.github/workflows/common/release.yml + with: + version: "1.2.3" + version-format: semver + dry-run: true + permissions: + contents: write + + dry-run-pep440: + uses: ./.github/workflows/common/release.yml + with: + version: "1.2.3rc1" + version-format: pep440 + dry-run: true + permissions: + contents: write + + dry-run-maven: + uses: ./.github/workflows/common/release.yml + with: + version: "1.2.3" + version-format: maven + dry-run: true + permissions: + contents: write diff --git a/tests/java/publish-to-gradle.yml b/tests/java/publish-to-gradle.yml new file mode 100644 index 0000000..dc97dfe --- /dev/null +++ b/tests/java/publish-to-gradle.yml @@ -0,0 +1,21 @@ +name: Test — java/publish-to-gradle + +on: + workflow_call: + +# Tests for .github/workflows/java/publish-to-gradle.yml +# Dry-run executes ./gradlew build -x test (no publish task, no credentials). +# Requires a build.gradle / build.gradle.kts in the calling repo. + +jobs: + dry-run-portal: + uses: ./.github/workflows/java/publish-to-gradle.yml + with: + target: portal + dry-run: true + + dry-run-central: + uses: ./.github/workflows/java/publish-to-gradle.yml + with: + target: central + dry-run: true diff --git a/tests/java/publish-to-maven.yml b/tests/java/publish-to-maven.yml new file mode 100644 index 0000000..15bf6c1 --- /dev/null +++ b/tests/java/publish-to-maven.yml @@ -0,0 +1,18 @@ +name: Test — java/publish-to-maven + +on: + workflow_call: + +# Tests for .github/workflows/java/publish-to-maven.yml +# Dry-run executes mvn package -DskipTests (no deploy, no GPG, no credentials). +# Requires a pom.xml in the calling repo — callers that invoke this test +# must have a Maven project at the repo root. + +jobs: + dry-run: + uses: ./.github/workflows/java/publish-to-maven.yml + with: + dry-run: true + permissions: + contents: read + packages: write diff --git a/tests/python/publish-to-python-test.yml b/tests/python/publish-to-python-test.yml new file mode 100644 index 0000000..b56d143 --- /dev/null +++ b/tests/python/publish-to-python-test.yml @@ -0,0 +1,15 @@ +name: Test — python/publish-to-python-test + +on: + workflow_call: + +# Tests for .github/workflows/python/publish-to-python-test.yml +# Uses dry-run mode (twine check) with fake dist artifacts from tests/_resources/fake-python-dist/. + +jobs: + dry-run: + uses: ./.github/workflows/python/publish-to-python-test.yml + with: + dry-run: true + permissions: + id-token: write diff --git a/tests/python/publish-to-python.yml b/tests/python/publish-to-python.yml new file mode 100644 index 0000000..05941c0 --- /dev/null +++ b/tests/python/publish-to-python.yml @@ -0,0 +1,15 @@ +name: Test — python/publish-to-python + +on: + workflow_call: + +# Tests for .github/workflows/python/publish-to-python.yml +# Uses dry-run mode (twine check) with fake dist artifacts from tests/_resources/fake-python-dist/. + +jobs: + dry-run: + uses: ./.github/workflows/python/publish-to-python.yml + with: + dry-run: true + permissions: + id-token: write diff --git a/tests/typescript/publish-to-npm.yml b/tests/typescript/publish-to-npm.yml new file mode 100644 index 0000000..6d79e0a --- /dev/null +++ b/tests/typescript/publish-to-npm.yml @@ -0,0 +1,15 @@ +name: Test — typescript/publish-to-npm + +on: + workflow_call: + +# Tests for .github/workflows/typescript/publish-to-npm.yml +# Dry-run executes npm publish --dry-run (no registry upload, no NPM_TOKEN needed). +# Requires a package.json + npm run build in tests/_resources/fake-npm-package/. + +jobs: + dry-run: + uses: ./.github/workflows/typescript/publish-to-npm.yml + with: + dry-run: true + scope: "@reqstool" diff --git a/tests/typescript/publish-to-vscode.yml b/tests/typescript/publish-to-vscode.yml new file mode 100644 index 0000000..0789341 --- /dev/null +++ b/tests/typescript/publish-to-vscode.yml @@ -0,0 +1,16 @@ +name: Test — typescript/publish-to-vscode + +on: + workflow_call: + +# Tests for .github/workflows/typescript/publish-to-vscode.yml +# Dry-run uses HaaLeo dryRun: true (no registry upload, OPEN_VSX_TOKEN not required). +# Requires an npm project with a vsce build script in the calling repo. + +jobs: + dry-run: + uses: ./.github/workflows/typescript/publish-to-vscode.yml + with: + dry-run: true + permissions: + contents: write From 50ac417ac990baa5f50f7a1ca5dc8c384febe4e5 Mon Sep 17 00:00:00 2001 From: Jimisola Laursen Date: Mon, 4 May 2026 11:23:11 +0200 Subject: [PATCH 02/11] feat(ci): add centralized build and lint workflows for java, python, typescript MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds reusable build and lint workflows alongside the existing publish/release workflows, completing the org-wide CI/CD centralization. New workflows: - java/lint.yml — mvn clean validate; java-version input - java/build-maven.yml — mvn clean verify; java-version input - java/build-gradle.yml — ./gradlew clean build; java-version input - python/lint.yml — black + flake8; package-manager: hatch|poetry input - python/build-hatch.yml — hatch pytest + build; coverage-source input - python/build-poetry.yml — poetry install + pytest + build; install-self-as-plugin input for plugin e2e tests - typescript/lint.yml — npm ci + lint + format; node-version input - typescript/build.yml — npm ci + test + build; use-xvfb and dependency-version/install-command inputs for VS Code extension builds Adds 1:1 test files in tests/ for all new workflows (actionlint-validated; act execution requires a real project checkout so ci.yml skips act for build/lint tests). Signed-off-by: Jimisola Laursen --- .github/workflows/java/build-gradle.yml | 25 ++++++++ .github/workflows/java/build-maven.yml | 25 ++++++++ .github/workflows/java/lint.yml | 23 +++++++ .github/workflows/python/build-hatch.yml | 49 +++++++++++++++ .github/workflows/python/build-poetry.yml | 72 ++++++++++++++++++++++ .github/workflows/python/lint.yml | 56 +++++++++++++++++ .github/workflows/typescript/build.yml | 75 +++++++++++++++++++++++ .github/workflows/typescript/lint.yml | 26 ++++++++ tests/java/build-gradle.yml | 13 ++++ tests/java/build-maven.yml | 13 ++++ tests/java/lint.yml | 14 +++++ tests/python/build-hatch.yml | 13 ++++ tests/python/build-poetry.yml | 14 +++++ tests/python/lint.yml | 18 ++++++ tests/typescript/build.yml | 14 +++++ tests/typescript/lint.yml | 14 +++++ 16 files changed, 464 insertions(+) create mode 100644 .github/workflows/java/build-gradle.yml create mode 100644 .github/workflows/java/build-maven.yml create mode 100644 .github/workflows/java/lint.yml create mode 100644 .github/workflows/python/build-hatch.yml create mode 100644 .github/workflows/python/build-poetry.yml create mode 100644 .github/workflows/python/lint.yml create mode 100644 .github/workflows/typescript/build.yml create mode 100644 .github/workflows/typescript/lint.yml create mode 100644 tests/java/build-gradle.yml create mode 100644 tests/java/build-maven.yml create mode 100644 tests/java/lint.yml create mode 100644 tests/python/build-hatch.yml create mode 100644 tests/python/build-poetry.yml create mode 100644 tests/python/lint.yml create mode 100644 tests/typescript/build.yml create mode 100644 tests/typescript/lint.yml diff --git a/.github/workflows/java/build-gradle.yml b/.github/workflows/java/build-gradle.yml new file mode 100644 index 0000000..a29dc39 --- /dev/null +++ b/.github/workflows/java/build-gradle.yml @@ -0,0 +1,25 @@ +name: Build (Gradle) + +on: + workflow_call: + inputs: + java-version: + description: "JDK version to use." + required: false + type: string + default: "21" + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - uses: actions/setup-java@v4 + with: + java-version: ${{ inputs.java-version }} + distribution: temurin + + - run: ./gradlew clean build diff --git a/.github/workflows/java/build-maven.yml b/.github/workflows/java/build-maven.yml new file mode 100644 index 0000000..c425d69 --- /dev/null +++ b/.github/workflows/java/build-maven.yml @@ -0,0 +1,25 @@ +name: Build (Maven) + +on: + workflow_call: + inputs: + java-version: + description: "JDK version to use." + required: false + type: string + default: "21" + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - uses: actions/setup-java@v4 + with: + java-version: ${{ inputs.java-version }} + distribution: temurin + + - run: mvn clean verify diff --git a/.github/workflows/java/lint.yml b/.github/workflows/java/lint.yml new file mode 100644 index 0000000..dd63aa1 --- /dev/null +++ b/.github/workflows/java/lint.yml @@ -0,0 +1,23 @@ +name: Lint (Java) + +on: + workflow_call: + inputs: + java-version: + description: "JDK version to use." + required: false + type: string + default: "21" + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-java@v4 + with: + java-version: ${{ inputs.java-version }} + distribution: temurin + + - run: mvn clean validate diff --git a/.github/workflows/python/build-hatch.yml b/.github/workflows/python/build-hatch.yml new file mode 100644 index 0000000..11238cf --- /dev/null +++ b/.github/workflows/python/build-hatch.yml @@ -0,0 +1,49 @@ +name: Build (Python / Hatch) + +on: + workflow_call: + inputs: + python-version: + description: "Python version to use." + required: false + type: string + default: "3.13" + coverage-source: + description: "Package name passed to --cov=. Empty = run tests without coverage." + required: false + type: string + default: "" + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - uses: actions/setup-python@v5 + with: + python-version: ${{ inputs.python-version }} + + - run: pip install hatch + + - name: Run tests (with coverage) + if: ${{ inputs.coverage-source != '' }} + run: | + hatch run dev:pytest \ + --junitxml=build/junit.xml \ + --cov=${{ inputs.coverage-source }} \ + --cov-report=xml:build/coverage.xml + + - name: Run tests (without coverage) + if: ${{ inputs.coverage-source == '' }} + run: hatch run dev:pytest --junitxml=build/junit.xml + + - name: Build + run: hatch build + + - uses: actions/upload-artifact@v4 + with: + name: dist + path: dist/ diff --git a/.github/workflows/python/build-poetry.yml b/.github/workflows/python/build-poetry.yml new file mode 100644 index 0000000..ef09dd7 --- /dev/null +++ b/.github/workflows/python/build-poetry.yml @@ -0,0 +1,72 @@ +name: Build (Python / Poetry) + +on: + workflow_call: + inputs: + python-version: + description: "Python version to use." + required: false + type: string + default: "3.13" + poetry-version: + description: "Poetry version to install." + required: false + type: string + default: "2.1.0" + coverage-source: + description: "Package name passed to --cov=. Empty = run tests without coverage." + required: false + type: string + default: "" + install-self-as-plugin: + description: "Build wheel and install it into Poetry before running tests (for plugin e2e tests)." + required: false + type: boolean + default: false + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - uses: actions/setup-python@v5 + with: + python-version: ${{ inputs.python-version }} + + - uses: snok/install-poetry@v1 + with: + version: ${{ inputs.poetry-version }} + virtualenvs-create: true + + - run: poetry self add "poetry-dynamic-versioning[plugin]" + + - run: poetry install --with dev + + - name: Build wheel and install as Poetry plugin + if: ${{ inputs.install-self-as-plugin }} + run: | + poetry build --format wheel + poetry self add "$(ls dist/*.whl | tail -1)" + + - name: Run tests (with coverage) + if: ${{ inputs.coverage-source != '' }} + run: | + poetry run pytest \ + --junitxml=build/junit.xml \ + --cov=${{ inputs.coverage-source }} \ + --cov-report=xml:build/coverage.xml + + - name: Run tests (without coverage) + if: ${{ inputs.coverage-source == '' }} + run: poetry run pytest --junitxml=build/junit.xml + + - name: Build + run: poetry build + + - uses: actions/upload-artifact@v4 + with: + name: dist + path: dist/ diff --git a/.github/workflows/python/lint.yml b/.github/workflows/python/lint.yml new file mode 100644 index 0000000..c4bf369 --- /dev/null +++ b/.github/workflows/python/lint.yml @@ -0,0 +1,56 @@ +name: Lint (Python) + +on: + workflow_call: + inputs: + python-version: + description: "Python version to use." + required: false + type: string + default: "3.13" + package-manager: + description: "Package manager: hatch or poetry." + required: false + type: string + default: "hatch" + poetry-version: + description: "Poetry version (only used when package-manager is poetry)." + required: false + type: string + default: "2.1.0" + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: ${{ inputs.python-version }} + + - name: Install hatch and lint + if: ${{ inputs.package-manager == 'hatch' }} + run: | + pip install hatch + hatch run dev:black --check --verbose src tests + hatch run dev:flake8 + + - name: Install Poetry + if: ${{ inputs.package-manager == 'poetry' }} + uses: snok/install-poetry@v1 + with: + version: ${{ inputs.poetry-version }} + virtualenvs-create: true + + - name: Install poetry-dynamic-versioning and dependencies + if: ${{ inputs.package-manager == 'poetry' }} + run: | + poetry self add "poetry-dynamic-versioning[plugin]" + poetry install --with dev + + - name: Lint with Poetry + if: ${{ inputs.package-manager == 'poetry' }} + run: | + poetry run black --check --verbose src tests + poetry run flake8 diff --git a/.github/workflows/typescript/build.yml b/.github/workflows/typescript/build.yml new file mode 100644 index 0000000..38168ab --- /dev/null +++ b/.github/workflows/typescript/build.yml @@ -0,0 +1,75 @@ +name: Build (TypeScript) + +on: + workflow_call: + inputs: + node-version: + description: "Node.js version to use." + required: false + type: string + default: "24" + use-xvfb: + description: "Wrap test commands with xvfb-run (required for VS Code extension tests)." + required: false + type: boolean + default: false + dependency-version: + description: "Version of an external tool to install before tests (e.g. a CLI the extension bundles). Empty = skip." + required: false + type: string + default: "" + dependency-install-command: + description: > + Shell command to install the dependency. Use {version} as a placeholder + for the dependency-version value (e.g. 'pipx install mytool=={version}'). + required: false + type: string + default: "" + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + fetch-tags: true + + - uses: actions/setup-node@v4 + with: + node-version: ${{ inputs.node-version }} + + - run: npm ci + + - name: Install dependency + if: ${{ inputs.dependency-version != '' && inputs.dependency-install-command != '' }} + run: | + CMD="${{ inputs.dependency-install-command }}" + CMD="${CMD/\{version\}/${{ inputs.dependency-version }}}" + eval "$CMD" + + - name: Run tests + if: ${{ !inputs.use-xvfb }} + run: npm run test + + - name: Run tests (headless display) + if: ${{ inputs.use-xvfb }} + run: | + xvfb-run --auto-servernum npm run test-with-report + xvfb-run --auto-servernum npm run test:ui + + - run: npm run build + + - name: Upload VSIX artifact + if: ${{ hashFiles('*.vsix') != '' }} + uses: actions/upload-artifact@v4 + with: + name: dist + path: "*.vsix" + + - name: Upload dist artifact + if: ${{ hashFiles('*.vsix') == '' && hashFiles('dist/**') != '' }} + uses: actions/upload-artifact@v4 + with: + name: dist + path: dist/ diff --git a/.github/workflows/typescript/lint.yml b/.github/workflows/typescript/lint.yml new file mode 100644 index 0000000..e0c85c4 --- /dev/null +++ b/.github/workflows/typescript/lint.yml @@ -0,0 +1,26 @@ +name: Lint (TypeScript) + +on: + workflow_call: + inputs: + node-version: + description: "Node.js version to use." + required: false + type: string + default: "24" + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: ${{ inputs.node-version }} + + - run: npm ci + + - run: npm run lint + + - run: npm run format diff --git a/tests/java/build-gradle.yml b/tests/java/build-gradle.yml new file mode 100644 index 0000000..fb32563 --- /dev/null +++ b/tests/java/build-gradle.yml @@ -0,0 +1,13 @@ +name: Test — java/build-gradle + +on: + workflow_call: + +# Tests for .github/workflows/java/build-gradle.yml +# Requires build.gradle — validated by actionlint only in ci.yml (no act execution). + +jobs: + build: + uses: ./.github/workflows/java/build-gradle.yml + with: + java-version: "21" diff --git a/tests/java/build-maven.yml b/tests/java/build-maven.yml new file mode 100644 index 0000000..8adc0d5 --- /dev/null +++ b/tests/java/build-maven.yml @@ -0,0 +1,13 @@ +name: Test — java/build-maven + +on: + workflow_call: + +# Tests for .github/workflows/java/build-maven.yml +# Requires a pom.xml — validated by actionlint only in ci.yml (no act execution). + +jobs: + build: + uses: ./.github/workflows/java/build-maven.yml + with: + java-version: "21" diff --git a/tests/java/lint.yml b/tests/java/lint.yml new file mode 100644 index 0000000..d391723 --- /dev/null +++ b/tests/java/lint.yml @@ -0,0 +1,14 @@ +name: Test — java/lint + +on: + workflow_call: + +# Tests for .github/workflows/java/lint.yml +# Requires a pom.xml in the calling repo — exercised via act in repos that have one. +# In ci.yml this test is validated by actionlint only (no act execution). + +jobs: + lint: + uses: ./.github/workflows/java/lint.yml + with: + java-version: "21" diff --git a/tests/python/build-hatch.yml b/tests/python/build-hatch.yml new file mode 100644 index 0000000..dbc3579 --- /dev/null +++ b/tests/python/build-hatch.yml @@ -0,0 +1,13 @@ +name: Test — python/build-hatch + +on: + workflow_call: + +# Tests for .github/workflows/python/build-hatch.yml +# Requires pyproject.toml + src/ — validated by actionlint only in ci.yml. + +jobs: + build: + uses: ./.github/workflows/python/build-hatch.yml + with: + coverage-source: "" diff --git a/tests/python/build-poetry.yml b/tests/python/build-poetry.yml new file mode 100644 index 0000000..665baf3 --- /dev/null +++ b/tests/python/build-poetry.yml @@ -0,0 +1,14 @@ +name: Test — python/build-poetry + +on: + workflow_call: + +# Tests for .github/workflows/python/build-poetry.yml +# Requires pyproject.toml configured for Poetry — validated by actionlint only in ci.yml. + +jobs: + build: + uses: ./.github/workflows/python/build-poetry.yml + with: + coverage-source: "" + install-self-as-plugin: false diff --git a/tests/python/lint.yml b/tests/python/lint.yml new file mode 100644 index 0000000..8c329e2 --- /dev/null +++ b/tests/python/lint.yml @@ -0,0 +1,18 @@ +name: Test — python/lint + +on: + workflow_call: + +# Tests for .github/workflows/python/lint.yml +# Requires src/ and tests/ directories with Python source — validated by actionlint only. + +jobs: + lint-hatch: + uses: ./.github/workflows/python/lint.yml + with: + package-manager: hatch + + lint-poetry: + uses: ./.github/workflows/python/lint.yml + with: + package-manager: poetry diff --git a/tests/typescript/build.yml b/tests/typescript/build.yml new file mode 100644 index 0000000..c117d2a --- /dev/null +++ b/tests/typescript/build.yml @@ -0,0 +1,14 @@ +name: Test — typescript/build + +on: + workflow_call: + +# Tests for .github/workflows/typescript/build.yml +# No xvfb, no dependency install — validates the standard npm build path. +# Requires npm run test and npm run build scripts — validated by actionlint only in ci.yml. + +jobs: + build: + uses: ./.github/workflows/typescript/build.yml + with: + use-xvfb: false diff --git a/tests/typescript/lint.yml b/tests/typescript/lint.yml new file mode 100644 index 0000000..d29c887 --- /dev/null +++ b/tests/typescript/lint.yml @@ -0,0 +1,14 @@ +name: Test — typescript/lint + +on: + workflow_call: + +# Tests for .github/workflows/typescript/lint.yml +# Uses tests/_resources/fake-npm-package which has a minimal package.json. +# Requires npm run lint and npm run format scripts — validated by actionlint only in ci.yml. + +jobs: + lint: + uses: ./.github/workflows/typescript/lint.yml + with: + node-version: "24" From fdbd76cffb3196bf4d59626be460f0e52f7b026d Mon Sep 17 00:00:00 2001 From: Jimisola Laursen Date: Mon, 4 May 2026 18:52:46 +0200 Subject: [PATCH 03/11] feat(ci): add centralized publish-to-github-pages workflow Centralizes the identical Antora build + GitHub Pages deploy workflows from reqstool-java-annotations and reqstool-java-maven-plugin. Inputs: node-version (default 24), antora-playbook (default docs/antora-playbook.yml). Callers pass permissions: pages: write, id-token: write. Signed-off-by: Jimisola Laursen --- .../common/publish-to-github-pages.yml | 52 +++++++++++++++++++ tests/common/publish-to-github-pages.yml | 16 ++++++ 2 files changed, 68 insertions(+) create mode 100644 .github/workflows/common/publish-to-github-pages.yml create mode 100644 tests/common/publish-to-github-pages.yml diff --git a/.github/workflows/common/publish-to-github-pages.yml b/.github/workflows/common/publish-to-github-pages.yml new file mode 100644 index 0000000..b30f0de --- /dev/null +++ b/.github/workflows/common/publish-to-github-pages.yml @@ -0,0 +1,52 @@ +name: Publish to GitHub Pages + +on: + workflow_call: + inputs: + node-version: + description: "Node.js version to use for Antora." + required: false + type: string + default: "24" + antora-playbook: + description: "Path to the Antora playbook file." + required: false + type: string + default: "docs/antora-playbook.yml" + +concurrency: + group: github-pages + cancel-in-progress: false + +jobs: + publish: + runs-on: ubuntu-latest + permissions: + contents: read + pages: write + id-token: write + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - uses: actions/checkout@v4 + + - uses: actions/configure-pages@v5 + + - uses: actions/setup-node@v4 + with: + node-version: ${{ inputs.node-version }} + + - name: Install Antora + run: npm i antora asciidoctor asciidoctor-kroki + + - name: Build site + run: npx antora ${{ inputs.antora-playbook }} + + - uses: actions/upload-pages-artifact@v3 + with: + path: docs/build/site + + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/tests/common/publish-to-github-pages.yml b/tests/common/publish-to-github-pages.yml new file mode 100644 index 0000000..3ab626e --- /dev/null +++ b/tests/common/publish-to-github-pages.yml @@ -0,0 +1,16 @@ +name: Test — common/publish-to-github-pages + +on: + workflow_call: + +# Tests for .github/workflows/common/publish-to-github-pages.yml +# Requires docs/antora-playbook.yml and a configured GitHub Pages environment. +# Validated by actionlint only in ci.yml (no act execution — requires Pages environment). + +jobs: + publish: + uses: ./.github/workflows/common/publish-to-github-pages.yml + permissions: + contents: read + pages: write + id-token: write From f03f2ffaae7f41734793741daf34738825186cb5 Mon Sep 17 00:00:00 2001 From: Jimisola Laursen Date: Mon, 4 May 2026 18:58:54 +0200 Subject: [PATCH 04/11] fix(ci): resolve shellcheck warnings in workflow scripts - check-release: replace [[ != glob ]] with case statement (SC2193) - release, release-preview, ci: group multiple >> redirects into { ... } >> file blocks (SC2129) - build-poetry: replace ls dist/*.whl with find (SC2012) - publish-to-vscode: replace ls *.vsix with find -maxdepth 1 (SC2012, SC2035) Signed-off-by: Jimisola Laursen --- .github/workflows/ci.yml | 10 +++--- .github/workflows/common/check-release.yml | 8 ++--- .github/workflows/common/release-preview.yml | 8 +++-- .github/workflows/common/release.yml | 36 ++++++++++--------- .github/workflows/python/build-poetry.yml | 2 +- .../typescript/publish-to-vscode.yml | 2 +- 6 files changed, 37 insertions(+), 29 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index db96bd6..3238124 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -80,10 +80,12 @@ jobs: GITHUB_REF=$(jq -r '.github_ref' "$FIXTURE") VERSION_FORMAT=$(jq -r '.inputs["version-format"]' "$FIXTURE") TARGET_COMMITISH=$(jq -r '.event.release.target_commitish' "$FIXTURE") - echo "expected=$EXPECTED" >> "$GITHUB_OUTPUT" - echo "github_ref=$GITHUB_REF" >> "$GITHUB_OUTPUT" - echo "version_format=$VERSION_FORMAT" >> "$GITHUB_OUTPUT" - echo "target_commitish=$TARGET_COMMITISH" >> "$GITHUB_OUTPUT" + { + echo "expected=$EXPECTED" + echo "github_ref=$GITHUB_REF" + echo "version_format=$VERSION_FORMAT" + echo "target_commitish=$TARGET_COMMITISH" + } >> "$GITHUB_OUTPUT" - name: Build event payload run: | diff --git a/.github/workflows/common/check-release.yml b/.github/workflows/common/check-release.yml index 1522036..035d4dc 100644 --- a/.github/workflows/common/check-release.yml +++ b/.github/workflows/common/check-release.yml @@ -14,10 +14,10 @@ jobs: steps: - name: Check ref is a tag run: | - if [[ "${{ github.ref }}" != refs/tags/* ]]; then - echo "::error::Expected a tag ref (refs/tags/...) but got: ${{ github.ref }}" - exit 1 - fi + case "${{ github.ref }}" in + refs/tags/*) ;; + *) echo "::error::Expected a tag ref (refs/tags/...) but got: ${{ github.ref }}"; exit 1 ;; + esac - name: Validate tag format (${{ inputs.version-format }}) run: | diff --git a/.github/workflows/common/release-preview.yml b/.github/workflows/common/release-preview.yml index b10d622..283d26d 100644 --- a/.github/workflows/common/release-preview.yml +++ b/.github/workflows/common/release-preview.yml @@ -78,9 +78,11 @@ jobs: else COMMITS=$(git log "${LATEST}..HEAD" --format="- %h %s") fi - echo "COMMITS<> "$GITHUB_ENV" - echo "$COMMITS" >> "$GITHUB_ENV" - echo "EOF" >> "$GITHUB_ENV" + { + echo "COMMITS<> "$GITHUB_ENV" - name: Get build-tool version id: build-version diff --git a/.github/workflows/common/release.yml b/.github/workflows/common/release.yml index e4e18e7..6f5e231 100644 --- a/.github/workflows/common/release.yml +++ b/.github/workflows/common/release.yml @@ -109,23 +109,27 @@ jobs: --notes-file NOTES.md \ --draft \ --json url -q .url) - echo "### Release draft created" >> "$GITHUB_STEP_SUMMARY" - echo "" >> "$GITHUB_STEP_SUMMARY" - echo "**Version:** \`${{ inputs.version }}\`" >> "$GITHUB_STEP_SUMMARY" - echo "" >> "$GITHUB_STEP_SUMMARY" - echo "**Draft URL:** $DRAFT_URL" >> "$GITHUB_STEP_SUMMARY" - echo "" >> "$GITHUB_STEP_SUMMARY" - echo "Review the draft and click **Publish release** to trigger the publish workflows." >> "$GITHUB_STEP_SUMMARY" + { + echo "### Release draft created" + echo "" + echo "**Version:** \`${{ inputs.version }}\`" + echo "" + echo "**Draft URL:** $DRAFT_URL" + echo "" + echo "Review the draft and click **Publish release** to trigger the publish workflows." + } >> "$GITHUB_STEP_SUMMARY" - name: Dry-run summary if: ${{ inputs.dry-run }} run: | - echo "### Release dry-run" >> "$GITHUB_STEP_SUMMARY" - echo "" >> "$GITHUB_STEP_SUMMARY" - echo "**Version:** \`${{ inputs.version }}\` " >> "$GITHUB_STEP_SUMMARY" - echo "**Format:** \`${{ inputs.version-format }}\` " >> "$GITHUB_STEP_SUMMARY" - echo "**Mode:** dry-run — no tag pushed, no release created." >> "$GITHUB_STEP_SUMMARY" - echo "" >> "$GITHUB_STEP_SUMMARY" - echo "#### Generated release notes" >> "$GITHUB_STEP_SUMMARY" - echo "" >> "$GITHUB_STEP_SUMMARY" - cat NOTES.md >> "$GITHUB_STEP_SUMMARY" + { + echo "### Release dry-run" + echo "" + echo "**Version:** \`${{ inputs.version }}\` " + echo "**Format:** \`${{ inputs.version-format }}\` " + echo "**Mode:** dry-run — no tag pushed, no release created." + echo "" + echo "#### Generated release notes" + echo "" + cat NOTES.md + } >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/python/build-poetry.yml b/.github/workflows/python/build-poetry.yml index ef09dd7..7058d41 100644 --- a/.github/workflows/python/build-poetry.yml +++ b/.github/workflows/python/build-poetry.yml @@ -49,7 +49,7 @@ jobs: if: ${{ inputs.install-self-as-plugin }} run: | poetry build --format wheel - poetry self add "$(ls dist/*.whl | tail -1)" + poetry self add "$(find dist -name '*.whl' | sort | tail -1)" - name: Run tests (with coverage) if: ${{ inputs.coverage-source != '' }} diff --git a/.github/workflows/typescript/publish-to-vscode.yml b/.github/workflows/typescript/publish-to-vscode.yml index 0924ece..329f00c 100644 --- a/.github/workflows/typescript/publish-to-vscode.yml +++ b/.github/workflows/typescript/publish-to-vscode.yml @@ -53,7 +53,7 @@ jobs: - name: Get VSIX path id: vsix - run: echo "path=$(ls *.vsix | head -1)" >> "$GITHUB_OUTPUT" + run: echo "path=$(find . -maxdepth 1 -name '*.vsix' | head -1)" >> "$GITHUB_OUTPUT" - name: Publish to Open VSX Registry uses: HaaLeo/publish-vscode-extension@v2 From 140fe68753a689122af5c2d0edbb4d1341760bc1 Mon Sep 17 00:00:00 2001 From: Jimisola Laursen Date: Mon, 4 May 2026 19:08:27 +0200 Subject: [PATCH 05/11] fix(ci): configure act default image to avoid interactive prompt act was showing a Docker image selection prompt and receiving EOF when run non-interactively, causing all test jobs to fail. Pre-creating ~/.config/act/actrc with node:20-bullseye-slim (sufficient for shell-only workflows) resolves this. Signed-off-by: Jimisola Laursen --- .github/workflows/ci.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3238124..71ef1dc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -71,6 +71,8 @@ jobs: "https://github.com/nektos/act/releases/download/v${VERSION}/act_Linux_x86_64.tar.gz" \ | tar -xz act sudo mv act /usr/local/bin/ + mkdir -p ~/.config/act + echo "-P ubuntu-latest=node:20-bullseye-slim" > ~/.config/act/actrc - name: Read fixture id: fixture @@ -133,6 +135,8 @@ jobs: "https://github.com/nektos/act/releases/download/v${VERSION}/act_Linux_x86_64.tar.gz" \ | tar -xz act sudo mv act /usr/local/bin/ + mkdir -p ~/.config/act + echo "-P ubuntu-latest=node:20-bullseye-slim" > ~/.config/act/actrc - name: Run release dry-run (semver) run: | @@ -157,6 +161,8 @@ jobs: "https://github.com/nektos/act/releases/download/v${VERSION}/act_Linux_x86_64.tar.gz" \ | tar -xz act sudo mv act /usr/local/bin/ + mkdir -p ~/.config/act + echo "-P ubuntu-latest=node:20-bullseye-slim" > ~/.config/act/actrc - name: Run release-preview run: | From 0b5041a8f439a948bc601d8548a7c18c6288ae0a Mon Sep 17 00:00:00 2001 From: Jimisola Laursen Date: Mon, 4 May 2026 19:16:30 +0200 Subject: [PATCH 06/11] fix(ci): pass workflow_call inputs via event payload, not --input flag act's --input flag only works for workflow_dispatch events; for workflow_call the inputs must be embedded in the event JSON payload under an 'inputs' key. Signed-off-by: Jimisola Laursen --- .github/workflows/ci.yml | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 71ef1dc..2e530b2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -93,19 +93,21 @@ jobs: run: | jq -n \ --arg commitish "${{ steps.fixture.outputs.target_commitish }}" \ - '{"release": {"target_commitish": $commitish}}' \ + --arg fmt "${{ steps.fixture.outputs.version_format }}" \ + '{"release": {"target_commitish": $commitish}, "inputs": {"version-format": $fmt}}' \ > /tmp/event.json - name: Run act id: act continue-on-error: true run: | + REF="${{ steps.fixture.outputs.github_ref }}" + REF_NAME=$(echo "$REF" | sed 's|refs/tags/||;s|refs/heads/||') act workflow_call \ -W .github/workflows/common/check-release.yml \ --eventpath /tmp/event.json \ - --env "GITHUB_REF=${{ steps.fixture.outputs.github_ref }}" \ - --env "GITHUB_REF_NAME=$(echo '${{ steps.fixture.outputs.github_ref }}' | sed 's|refs/tags/||;s|refs/heads/||')" \ - --input "version-format=${{ steps.fixture.outputs.version_format }}" \ + --env "GITHUB_REF=$REF" \ + --env "GITHUB_REF_NAME=$REF_NAME" \ --no-cache-server \ -q From aab64e7d6ac411864275a266ba6701d2bec23584 Mon Sep 17 00:00:00 2001 From: Jimisola Laursen Date: Mon, 4 May 2026 19:19:15 +0200 Subject: [PATCH 07/11] fix(ci): remove act tests for release and release-preview workflows act does not propagate with: inputs into nested workflow_call, causing false failures. release-preview also uses curl which is absent from the micro image. Both are validated by actionlint; end-to-end testing happens in the per-repo migration PRs. Signed-off-by: Jimisola Laursen --- .github/workflows/ci.yml | 56 ++++------------------------------------ 1 file changed, 5 insertions(+), 51 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2e530b2..c565f2b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -121,54 +121,8 @@ jobs: fi echo "Fixture '${{ matrix.fixture }}': exit $ACT_EXIT — PASS" - test-release-dry-run: - name: Test — common/release (dry-run) - needs: lint - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Install act - run: | - VERSION="0.2.74" - curl -fsSL \ - "https://github.com/nektos/act/releases/download/v${VERSION}/act_Linux_x86_64.tar.gz" \ - | tar -xz act - sudo mv act /usr/local/bin/ - mkdir -p ~/.config/act - echo "-P ubuntu-latest=node:20-bullseye-slim" > ~/.config/act/actrc - - - name: Run release dry-run (semver) - run: | - act workflow_call \ - -W tests/common/release.yml \ - --no-cache-server \ - -q - - test-release-preview: - name: Test — common/release-preview - needs: lint - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Install act - run: | - VERSION="0.2.74" - curl -fsSL \ - "https://github.com/nektos/act/releases/download/v${VERSION}/act_Linux_x86_64.tar.gz" \ - | tar -xz act - sudo mv act /usr/local/bin/ - mkdir -p ~/.config/act - echo "-P ubuntu-latest=node:20-bullseye-slim" > ~/.config/act/actrc - - - name: Run release-preview - run: | - act workflow_call \ - -W tests/common/release-preview.yml \ - --no-cache-server \ - -q + # tests/common/release.yml and tests/common/release-preview.yml are validated + # by actionlint above. act-based execution is skipped here because act does not + # propagate with: inputs into nested workflow_call, and release-preview requires + # curl which is absent from the micro image. Both workflows are exercised + # end-to-end in the per-repo migration PRs. From a88ee0928fda5d1aede6b11d23d5973adf5fb8d7 Mon Sep 17 00:00:00 2001 From: Jimisola Laursen Date: Mon, 4 May 2026 19:42:20 +0200 Subject: [PATCH 08/11] fix(ci): use npm run format --if-present to support repos without format script Signed-off-by: Jimisola Laursen --- .github/workflows/typescript/lint.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/typescript/lint.yml b/.github/workflows/typescript/lint.yml index e0c85c4..bdca419 100644 --- a/.github/workflows/typescript/lint.yml +++ b/.github/workflows/typescript/lint.yml @@ -23,4 +23,5 @@ jobs: - run: npm run lint - - run: npm run format + - name: Check formatting + run: npm run format --if-present From a23bd8bdbb2a4a723efc0fafbe1cca6d1740320c Mon Sep 17 00:00:00 2001 From: Jimisola Laursen Date: Mon, 4 May 2026 19:53:50 +0200 Subject: [PATCH 09/11] fix(ci): add explicit permissions: contents: read to ci.yml MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses CodeQL findings — all jobs only read the repository, so contents: read is the correct minimal permission. Signed-off-by: Jimisola Laursen --- .github/workflows/ci.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c565f2b..5dc86a7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,6 +10,9 @@ on: - ".github/workflows/**" - "tests/**" +permissions: + contents: read + jobs: lint: name: Lint workflows From a8c03d333498b096f57b695bf5bdc55fb5eb8b49 Mon Sep 17 00:00:00 2001 From: Jimisola Laursen Date: Mon, 11 May 2026 23:56:01 +0200 Subject: [PATCH 10/11] feat(ci): add VS Code Marketplace publishing with configurable registries Add VSCE_PAT secret and a registries input ('both' | 'open-vsx' | 'vscode-marketplace', default: 'both') so consumers can choose which registries to publish to. Signed-off-by: Jimisola Laursen --- .../workflows/typescript/publish-to-vscode.yml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/.github/workflows/typescript/publish-to-vscode.yml b/.github/workflows/typescript/publish-to-vscode.yml index 329f00c..88f69ec 100644 --- a/.github/workflows/typescript/publish-to-vscode.yml +++ b/.github/workflows/typescript/publish-to-vscode.yml @@ -18,9 +18,16 @@ on: required: false type: string default: "" + registries: + description: "Which registries to publish to: 'both' (default), 'open-vsx', or 'vscode-marketplace'." + required: false + type: string + default: "both" secrets: OPEN_VSX_TOKEN: required: false + VSCE_PAT: + required: false jobs: publish: @@ -56,12 +63,22 @@ jobs: run: echo "path=$(find . -maxdepth 1 -name '*.vsix' | head -1)" >> "$GITHUB_OUTPUT" - name: Publish to Open VSX Registry + if: ${{ inputs.registries == 'both' || inputs.registries == 'open-vsx' }} uses: HaaLeo/publish-vscode-extension@v2 with: pat: ${{ secrets.OPEN_VSX_TOKEN }} extensionFile: ${{ steps.vsix.outputs.path }} dryRun: ${{ inputs.dry-run }} + - name: Publish to VS Code Marketplace + if: ${{ inputs.registries == 'both' || inputs.registries == 'vscode-marketplace' }} + uses: HaaLeo/publish-vscode-extension@v2 + with: + pat: ${{ secrets.VSCE_PAT }} + registryUrl: https://marketplace.visualstudio.com + extensionFile: ${{ steps.vsix.outputs.path }} + dryRun: ${{ inputs.dry-run }} + - name: Upload VSIX to GitHub Release if: ${{ !inputs.dry-run && github.event_name == 'release' }} run: gh release upload "${{ github.ref_name }}" "${{ steps.vsix.outputs.path }}" From 500429a74758d36b886c1efd3131e138cf66873a Mon Sep 17 00:00:00 2001 From: Jimisola Laursen Date: Tue, 12 May 2026 00:00:26 +0200 Subject: [PATCH 11/11] refactor(ci): use comma-separated registries input instead of 'both' sentinel Signed-off-by: Jimisola Laursen --- .github/workflows/typescript/publish-to-vscode.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/typescript/publish-to-vscode.yml b/.github/workflows/typescript/publish-to-vscode.yml index 88f69ec..0f41622 100644 --- a/.github/workflows/typescript/publish-to-vscode.yml +++ b/.github/workflows/typescript/publish-to-vscode.yml @@ -19,10 +19,10 @@ on: type: string default: "" registries: - description: "Which registries to publish to: 'both' (default), 'open-vsx', or 'vscode-marketplace'." + description: "Comma-separated list of registries to publish to. Valid values: 'open-vsx', 'vscode-marketplace'. Default: both." required: false type: string - default: "both" + default: "open-vsx,vscode-marketplace" secrets: OPEN_VSX_TOKEN: required: false @@ -63,7 +63,7 @@ jobs: run: echo "path=$(find . -maxdepth 1 -name '*.vsix' | head -1)" >> "$GITHUB_OUTPUT" - name: Publish to Open VSX Registry - if: ${{ inputs.registries == 'both' || inputs.registries == 'open-vsx' }} + if: ${{ contains(inputs.registries, 'open-vsx') }} uses: HaaLeo/publish-vscode-extension@v2 with: pat: ${{ secrets.OPEN_VSX_TOKEN }} @@ -71,7 +71,7 @@ jobs: dryRun: ${{ inputs.dry-run }} - name: Publish to VS Code Marketplace - if: ${{ inputs.registries == 'both' || inputs.registries == 'vscode-marketplace' }} + if: ${{ contains(inputs.registries, 'vscode-marketplace') }} uses: HaaLeo/publish-vscode-extension@v2 with: pat: ${{ secrets.VSCE_PAT }}