azurerm_backup_policy_vm: duration_type in archived_restore_point is ignored/nonexistant, always shown as Months in Azure Portal #2166
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| --- | |
| name: TeamCity Run Tests on Comment | |
| # Lets maintainers trigger TeamCity acceptance test runs by commenting | |
| # '/test [-b] [-f] [-s service] <TestPrefix>' on a PR. Verifies terraform-azure team | |
| # membership (or the allow-list) before dispatching via katbyte/tctest. | |
| # -f/-force bypasses the max-builds-per-pr limit. | |
| # | |
| # SECURITY: tctest's captured stderr is only ever analyzed to pick a canned failure | |
| # message - it is never included in the comment body, since comment bodies bypass the | |
| # secret masking applied to workflow logs (e.g. TCTEST_SERVER appearing in an HTTP | |
| # error URL). tctest is invoked without --wait, so it never fetches TeamCity build | |
| # logs and no build/test output can ever be echoed back onto a public PR. | |
| on: | |
| issue_comment: | |
| types: [created] | |
| permissions: | |
| contents: read | |
| pull-requests: write | |
| issues: write | |
| jobs: | |
| check-team-membership: | |
| runs-on: ubuntu-latest | |
| # Only run on pull request comments that starts with /test | |
| if: github.event.issue.pull_request && startsWith(github.event.comment.body, '/test') | |
| outputs: | |
| is-team-member: ${{ steps.check-membership.outputs.is-member }} | |
| is-allowed-user: ${{ steps.check-allowed-user.outputs.is-allowed-user }} | |
| env: | |
| AUTHORIZED_TEAM: terraform-azure | |
| steps: | |
| - name: Check team membership | |
| id: check-membership | |
| uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 | |
| with: | |
| github-token: ${{ secrets.GH_MEMEBERSHIP_CHECK_TOKEN }} | |
| script: | | |
| const teamSlug = "${{ env.AUTHORIZED_TEAM }}"; | |
| const org = context.repo.owner; | |
| const username = context.actor; | |
| try { | |
| const response = await github.rest.teams.getMembershipForUserInOrg({ | |
| org: org, | |
| team_slug: teamSlug, | |
| username: username, | |
| }); | |
| const isMember = response.data.state === 'active'; | |
| core.setOutput('is-member', isMember); | |
| if (isMember) { | |
| core.info(`User ${username} is a member of team ${teamSlug}`); | |
| } else { | |
| core.warning(`User ${username} is not an active member of team ${teamSlug}`); | |
| } | |
| return isMember; | |
| } catch (error) { | |
| if (error.status === 404) { | |
| core.warning(`User ${username} is not a member of team ${teamSlug}`); | |
| core.setOutput('is-member', false); | |
| return false; | |
| } | |
| throw error; | |
| } | |
| - name: Check allowed users list | |
| id: check-allowed-user | |
| uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 | |
| env: | |
| TCTEST_ALLOWED_USERS: ${{ secrets.TCTEST_ALLOWED_USERS }} | |
| with: | |
| script: | | |
| const allowedUsersText = process.env.TCTEST_ALLOWED_USERS || ''; | |
| const username = context.actor; | |
| // Split by newlines and trim whitespace, filter out empty lines | |
| const allowedUsers = allowedUsersText | |
| .split('\n') | |
| .map(line => line.trim()) | |
| .filter(line => line.length > 0); | |
| const isAllowedUser = allowedUsers.includes(username); | |
| core.setOutput('is-allowed-user', isAllowedUser); | |
| if (isAllowedUser) { | |
| core.info(`User ${username} is in the allowed users list`); | |
| } else { | |
| core.warning(`User ${username} is not in the allowed users list`); | |
| } | |
| return isAllowedUser; | |
| run-tests: | |
| runs-on: ubuntu-latest | |
| needs: check-team-membership | |
| if: needs.check-team-membership.outputs.is-team-member == 'true' || needs.check-team-membership.outputs.is-allowed-user == 'true' | |
| env: | |
| TCTEST_SERVER: ${{ secrets.TCTEST_SERVER }} | |
| TCTEST_FILEREGEX: 'internal/services/[a-z]*/[_a-zA-Z]*(resource|data_source)' | |
| TCTEST_SKIP_QUEUE: 'true' | |
| TCTEST_TOKEN_TC: ${{ secrets.TCTEST_TOKEN_TC }} | |
| TCTEST_BUILD_TYPE_ID: TF_AzureRM_AZURERM_SERVICE_PUBLIC | |
| TCTEST_REPO: terraform-providers/terraform-provider-azurerm | |
| steps: | |
| - name: Setup Go | |
| uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6 | |
| with: | |
| go-version: '1.26' | |
| - name: Cache tctest binary | |
| id: cache-tctest | |
| uses: actions/cache@caa296126883cff596d87d8935842f9db880ef25 # v5 | |
| with: | |
| path: ~/go/bin/tctest | |
| # keep this key in sync with the pinned version installed below | |
| key: tctest-${{ runner.os }}-v1.2.0 | |
| - name: Install tctest | |
| if: steps.cache-tctest.outputs.cache-hit != 'true' | |
| run: | | |
| go install github.com/katbyte/tctest@v1.2.0 | |
| - name: Get PR commit SHA | |
| id: get-pr-sha | |
| uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 | |
| with: | |
| github-token: ${{ secrets.GITHUB_TOKEN }} | |
| script: | | |
| const pr = await github.rest.pulls.get({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| pull_number: context.issue.number | |
| }); | |
| const sha = pr.data.head.sha; | |
| core.setOutput('sha', sha); | |
| core.info(`PR head SHA: ${sha}`); | |
| return sha; | |
| - name: Run tctest | |
| id: run-tctest | |
| env: | |
| # Passed via env rather than interpolated into the script to prevent | |
| # shell injection from untrusted comment text (OpenSSF Dangerous-Workflow) | |
| COMMENT_BODY: ${{ github.event.comment.body }} | |
| # tctest reads GITHUB_TOKEN for its GitHub API calls - without it, test | |
| # discovery runs unauthenticated (60 req/hr per runner IP) and hits 403s | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| PR_NUMBER=${{ github.event.issue.number }} | |
| PR_SHA="${{ steps.get-pr-sha.outputs.sha }}" | |
| # Parse flags from comment | |
| # Extract everything after /test | |
| FLAGS_STRING=$(echo "$COMMENT_BODY" | sed 's|^/test[[:space:]]*||') | |
| # Initialize default values | |
| beta=false | |
| force=false | |
| service="" | |
| # Parse flags using getopts | |
| # Split FLAGS_STRING on whitespace WITHOUT eval - eval would execute | |
| # command substitutions embedded in the (untrusted) comment text | |
| read -ra FLAGS <<< "$FLAGS_STRING" | |
| # getopts only handles single-char flags, so map the long spellings to -f | |
| for i in "${!FLAGS[@]}"; do | |
| case "${FLAGS[$i]}" in | |
| -force|--force) FLAGS[$i]="-f" ;; | |
| esac | |
| done | |
| set -- "${FLAGS[@]}" | |
| while getopts ":bfs:" opt; do | |
| case "$opt" in | |
| b) beta=true ;; | |
| f) force=true ;; | |
| s) service="$OPTARG" ;; | |
| *) | |
| echo "Usage: /test [-b] [-f] [-s service_name]" | |
| echo " -b: Test beta version" | |
| echo " -f: Force - bypass the max-builds-per-pr limit" | |
| echo " -s: Specify service name" | |
| exit 1 | |
| ;; | |
| esac | |
| done | |
| echo "Running tctest for PR #${PR_NUMBER}" | |
| echo "PR commit SHA: ${PR_SHA}" | |
| echo "Beta version: ${beta}" | |
| echo "Force: ${force}" | |
| echo "Service: ${service:-all}" | |
| extra_flags=() | |
| if [ "$service" != "" ]; then | |
| extra_flags+=(--service "$service") | |
| fi | |
| if [ "$force" = true ]; then | |
| extra_flags+=(--max-builds-per-pr 0) | |
| fi | |
| builds='[]' | |
| err_log=$(mktemp) | |
| # Always publish captured stderr so the failure comment can say why tctest failed, | |
| # even when a failing run_tctest exits the script via set -e | |
| finish() { | |
| { | |
| echo "errors<<TCTEST_ERR_EOF" | |
| cat "$err_log" | |
| echo "TCTEST_ERR_EOF" | |
| } >> "$GITHUB_OUTPUT" | |
| } | |
| trap finish EXIT | |
| run_tctest() { | |
| local out rc=0 err | |
| err=$(mktemp) | |
| out=$(tctest pr "${PR_NUMBER}" -c -q --reappend-split-character --build-type-id-add-service-suffix --properties "TRACKING_ID=${PR_SHA}" "$@" --json 2>"$err") || rc=$? | |
| cat "$err" >&2 | |
| cat "$err" >> "$err_log" | |
| rm -f "$err" | |
| if [ "$rc" -ne 0 ]; then | |
| exit "$rc" | |
| fi | |
| echo "$out" | |
| builds=$(printf '%s\n%s\n' "$builds" "${out:-[]}" | jq -s 'add') | |
| } | |
| run_tctest "${extra_flags[@]}" | |
| if [ "$beta" = true ]; then | |
| run_tctest "${extra_flags[@]}" --build-type-id TF_AzureRM_AZURERM_BETA_VERSION_SERVICE_PUBLIC | |
| fi | |
| { | |
| echo "builds<<TCTEST_BUILDS_EOF" | |
| echo "$builds" | |
| echo "TCTEST_BUILDS_EOF" | |
| } >> "$GITHUB_OUTPUT" | |
| - name: Comment on success | |
| if: success() | |
| uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 | |
| env: | |
| BUILDS_JSON: ${{ steps.run-tctest.outputs.builds }} | |
| with: | |
| github-token: ${{ secrets.GITHUB_TOKEN }} | |
| script: | | |
| await github.rest.reactions.createForIssueComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| comment_id: context.payload.comment.id, | |
| content: 'rocket' | |
| }); | |
| // hide previous /test failure comments now that a run has succeeded | |
| try { | |
| const comments = await github.paginate(github.rest.issues.listComments, { | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: context.issue.number, | |
| per_page: 100 | |
| }); | |
| const failures = comments.filter(c => | |
| c.user?.type === 'Bot' && | |
| c.body?.startsWith('❌') && | |
| (c.body.includes('TeamCity') || c.body.includes('would trigger builds')) | |
| ); | |
| for (const c of failures) { | |
| await github.graphql( | |
| `mutation($id: ID!) { | |
| minimizeComment(input: { subjectId: $id, classifier: OUTDATED }) { | |
| minimizedComment { isMinimized } | |
| } | |
| }`, | |
| { id: c.node_id } | |
| ); | |
| } | |
| if (failures.length > 0) { | |
| core.info(`Minimized ${failures.length} previous failure comment(s)`); | |
| } | |
| } catch (error) { | |
| core.warning(`Unable to minimize previous failure comments: ${error.message}`); | |
| } | |
| let builds = []; | |
| try { | |
| builds = JSON.parse(process.env.BUILDS_JSON || '[]'); | |
| } catch (error) { | |
| core.warning(`Unable to parse tctest build results: ${error.message}`); | |
| } | |
| if (Array.isArray(builds) && builds.length > 0) { | |
| const lines = builds.map(b => { | |
| const label = b.service ? `\`${b.service}\`` : 'build'; | |
| return `- ${label}: [#${b.build_number}](${b.url})`; | |
| }); | |
| const appended = `\n\n---\n**TeamCity build${builds.length > 1 ? 's' : ''} triggered:**\n${lines.join('\n')}`; | |
| await github.rest.issues.updateComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| comment_id: context.payload.comment.id, | |
| body: `${context.payload.comment.body}${appended}` | |
| }); | |
| } | |
| - name: Comment on failure | |
| if: failure() | |
| uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 | |
| env: | |
| TCTEST_ERRORS: ${{ steps.run-tctest.outputs.errors }} | |
| with: | |
| github-token: ${{ secrets.GITHUB_TOKEN }} | |
| script: | | |
| const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`; | |
| // strip ANSI colour codes from captured tctest stderr | |
| const errors = (process.env.TCTEST_ERRORS || '').replace(/\u001b\[[0-9;]*m/g, '').trim(); | |
| let body; | |
| // matchers are deliberately loose so minor rewording in tctest doesn't | |
| // silently drop us back to the generic failure comment | |
| const limit = errors.match(/would trigger (\d+)\b.*?\bexceeding\b.*?\blimit of (\d+)/i); | |
| if (limit) { | |
| body = `❌ This would trigger builds on **${limit[1]}** services, exceeding the limit of ${limit[2]}.\n\n` + | |
| `Re-run with \`/test -f\` (or \`/test -force\`) to run them anyway.`; | |
| } else if (/merge conflict/i.test(errors)) { | |
| body = '❌ This PR has merge conflicts — TeamCity tests cannot run until they are resolved.\n\n' + | |
| 'Merge the base branch (or rebase), then comment `/test` again.'; | |
| } else if (/rate limit/i.test(errors)) { | |
| body = '❌ Test discovery hit the GitHub API rate limit.\n\n' + | |
| 'Wait a few minutes and comment `/test` again.'; | |
| } else if (errors.includes('no builds were triggered')) { | |
| body = '❌ No TeamCity builds were triggered — no acceptance tests could be discovered from this PR\'s changed files.\n\n' + | |
| 'If this PR does have acceptance tests, re-run with an explicit prefix, e.g. `/test TestAccFoo`.'; | |
| } else { | |
| // unrecognized error: never post raw stderr (comment bodies bypass the | |
| // secret masking that workflow logs get) - point at the logs instead | |
| body = '❌ Failed to trigger TeamCity tests.'; | |
| } | |
| body += `\n\nSee the [workflow logs](${runUrl}) for details.`; | |
| await github.rest.issues.createComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: context.issue.number, | |
| body | |
| }); | |
| unauthorized-comment: | |
| runs-on: ubuntu-latest | |
| needs: check-team-membership | |
| if: needs.check-team-membership.outputs.is-team-member == 'false' && needs.check-team-membership.outputs.is-allowed-user == 'false' | |
| steps: | |
| - name: Comment unauthorized | |
| uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 | |
| with: | |
| github-token: ${{ secrets.GITHUB_TOKEN }} | |
| script: | | |
| await github.rest.issues.createComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: context.issue.number, | |
| body: '⚠️ You are not authorized to run tests. Only members of the designated team can trigger test runs.' | |
| }); |