Skip to content

azurerm_backup_policy_vm: duration_type in archived_restore_point is ignored/nonexistant, always shown as Months in Azure Portal #2166

azurerm_backup_policy_vm: duration_type in archived_restore_point is ignored/nonexistant, always shown as Months in Azure Portal

azurerm_backup_policy_vm: duration_type in archived_restore_point is ignored/nonexistant, always shown as Months in Azure Portal #2166

---
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.'
});